完善sku

This commit is contained in:
wangxiaowei
2025-06-04 18:04:25 +08:00
parent c1cae803e0
commit 7c43b4d6b8
11 changed files with 474 additions and 2652 deletions

View File

@ -34,3 +34,26 @@ export function checkCategory() {
export function uploadImage(params: any) {
return request.post({ url: '/upload/image', params })
}
export type SkuNameValue = {
value: string
image: string
}
export type SkuNameList = {
name: string
value: SkuNameValue[]
has_image?: number
}
export type SkuItemList = {
id?: number | string
ids?: number[]
image?: string
sku_value_arr: string[]
price: string
line_price: string
market_price: string
stock: number
weight: number
volume: string
code: string
}

View File

@ -19,6 +19,18 @@ export const isEmpty = (value: unknown) => {
return value == null && typeof value == 'undefined'
}
/**
* @description 数组扁平化
* @param arr { Array } 扁平化对象
* @return { Array } 扁平化后的数组
*/
export const flatten = (arr: any[]): any[] => {
return arr.reduce((result, item) => {
return result.concat(Array.isArray(item) ? flatten(item) : item)
}, [])
}
/**
* @description 树转数组,队列实现广度优先遍历
* @param {Array} data 数据
@ -185,9 +197,9 @@ export const calcColor = (color: string, opacity: number): string => {
const fullHex =
hex.length === 3
? hex
.split('')
.map((h) => h + h)
.join('')
.split('')
.map((h) => h + h)
.join('')
: hex
// 转换为 RGB

View File

@ -1,794 +0,0 @@
<template>
<div>
<div class="sku-form-container" :class="`sku-form-container-${theme}`" v-if="!disabled">
<div class="sku-form-section" v-for="(attrItem, attrIndex) in myAttribute" :key="attrIndex">
<div class="flex items-center">
<div class="sku-form-title">{{ attrItem.name }}</div>
<div @click="deleteAttrItemName(attrIndex)" class="mt-1 ml-2 cursor-pointer">
<el-icon>
<Delete />
</el-icon>
</div>
</div>
<div class="sku-form-tags-box">
<el-checkbox-group v-model="checked[attrIndex]" :disabled="disabled" class="checkbox-group">
<el-checkbox v-for="(item, index) in attrItem.item" :key="index" :disabled="disabled"
:label="item.name" @change="checked => onCheckedChange(attrIndex, index, checked)">
<div class="sku-checkbox-content">
<img v-if="item.image" :src="item.image" class="sku-option-image" :alt="item.name" />
<span>{{ item.name }}</span>
</div>
</el-checkbox>
</el-checkbox-group>
</div>
<div class="sku-form-add-tags" v-if="attrItem.canAddAttribute">
<el-input v-model="inputValues[attrIndex]" placeholder="请输入规格名称" size="small"
@keyup.enter="onAddAttribute(attrIndex)">
<template #append>
<el-button :icon="Plus" @click="onAddAttribute(attrIndex)">添加</el-button>
</template>
</el-input>
</div>
</div>
</div>
<el-form v-if="myAttribute.length > 0" ref="formRef" :model="form" :rules="rules" class="sku-form-table"
:class="disabled ? 'sku-form-table-disabled' : ''">
<el-table :data="form.skuData" border style="width: 100%;overflow-x: auto;" :key="form.skuData.length">
<el-table-column v-for="(col, colIndex) in emitAttribute" :key="colIndex" :label="col.name"
align="center">
<template #default="{ row }">
<div class="sku-table-cell">
<img v-if="getAttributeImage(col.name, row[col.name])"
:src="getAttributeImage(col.name, row[col.name])" class="sku-table-image" />
<span>{{ row[col.name] }} --{{ row[col.price] }}</span>
</div>
</template>
</el-table-column>
<el-table-column v-for="(col, colIndex) in structure" :key="colIndex" :label="col.label" align="center">
<template #header v-if="col.batch !== false && col.type === 'input' && isBatch">
<div class="sku-form-batch">
<el-input v-model="batch[col.name]" :placeholder="`批量${col.label}`"
@change="onBatchSet(col.name)">
<!-- <template #append>
<el-button @click="onBatchSet(col.name)">设置</el-button>
</template> -->
</el-input>
</div>
</template>
<template #default="{ row, $index }">
<div v-if="col.type === 'slot'">
<slot :name="col.name" :row="row" :index="$index" />
</div>
<el-form-item v-else :prop="`skuData.${$index}.${col.name}`"
:class="`sku-form-${$index}-${col.name}`">
<el-tooltip v-if="col.tips" :content="col.tips" placement="top" :hide-after="0">
<el-icon class="sku-form-tips">
<InfoFilled />
</el-icon>
</el-tooltip>
<el-input v-model="row[col.name]" size="small" :placeholder="col.placeholder"
:disabled="col.disabled" />
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, toRefs, nextTick } from 'vue'
import { Plus, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
/**
* 原始规格数据
* sourceAttribute: [
* { name: '颜色', item: ['黑', '金', '白'] },
* { name: '内存', item: ['16G', '32G'] },
* { name: '运营商', item: ['电信', '移动', '联通'] }
* ]
*/
sourceAttribute: {
type: Array,
default: () => []
},
/**
* 已使用的规格数据用于复原数据支持v-model:attribute修饰符
* attribute: [
* { name: '颜色', item: ['黑'] },
* { name: '运营商', item: ['电信', '移动', '联通'] }
* ]
*/
attribute: {
type: Array,
default: () => []
},
/**
* 用于复原sku数据支持v-model:sku修饰符
* sku: [
* { sku: '黑;电信', price: 1, stock: 1 },
* { sku: '黑;移动', price: 2, stock: 2 },
* { sku: '黑;联通', price: 3, stock: 3 }
* ]
*/
sku: {
type: Array,
default: () => []
},
/**
* 表格结构注意name字段用于输出sku数据
*/
structure: {
type: Array,
default: () => [
{ name: 'price', type: 'input', label: '价格' },
{ name: 'cost_price', type: 'input', label: '成本价' },
{ name: 'stock', type: 'input', label: '库存' }
]
},
// sku 字段分隔符
separator: {
type: String,
default: ';'
},
// 无规格的 sku
emptySku: {
type: String,
default: ''
},
// 是否显示 sku 选择栏
disabled: {
type: Boolean,
default: false
},
// 主题风格
theme: {
type: Number,
default: 1
},
// 是否开启异步加载
async: {
type: Boolean,
default: false
},
// 是否可添加属性值
canAddAttribute: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:attribute', 'update:sku', 'validate'])
// 使用toRefs优化props解构保持响应性
const { sourceAttribute, attribute, sku, structure, separator, emptySku, async: isAsync, canAddAttribute } = toRefs(props)
// 表单引用
const formRef = ref(null)
const isInit = ref(false)
// 输入框的值独立管理,避免深层次响应式问题
const inputValues = ref([])
// 数据
const form = reactive({
skuData: []
})
// 批量设置暂存数据
const batch = reactive({})
// 属性数据(包含选中状态)
const myAttribute = ref([])
// 用于管理checkbox组的选中状态
const checked = ref([])
// 计算规则
const rules = computed(() => {
const result = {}
structure.value.forEach(item => {
if (item.required) {
const rule = { required: true, message: `请输入${item.label}`, trigger: 'blur' }
if (item.validator) {
rule.validator = item.validator
}
result[item.name] = [rule]
} else if (item.validator) {
result[item.name] = [{ validator: item.validator, trigger: 'blur' }]
}
})
return result
})
// 是否显示批量设置
const isBatch = computed(() => {
return structure.value.some(item => item.type === 'input' && item.batch !== false)
})
// 仅输出勾选的属性
const emitAttribute = computed(() => {
return myAttribute.value.map(attr => {
// 过滤选中的项目并包含完整的属性信息
const selectedItems = attr.item
.filter(item => item.checked)
.map(item => {
// 仅移除checked状态保留其他所有属性
const { checked, ...itemDetail } = item;
return itemDetail;
});
return {
name: attr.name,
item: selectedItems
};
}).filter(attr => attr.item.length > 0);
})
// 初始化方法
const init = () => {
nextTick(() => {
isInit.value = true
// 初始化 myAttribute
const newMyAttribute = []
// 根据 sourceAttribute 复原 myAttribute 的结构
sourceAttribute.value.forEach(v => {
const temp = {
name: v.name,
canAddAttribute: typeof v.canAddAttribute !== 'undefined' ? v.canAddAttribute : canAddAttribute.value,
addAttribute: '',
item: []
}
// 处理规格项,支持字符串或对象格式
v.item.forEach(itemValue => {
if (typeof itemValue === 'string') {
// 处理字符串类型的规格项(向后兼容)
temp.item.push({
name: itemValue,
checked: false
})
} else {
// 处理对象类型的规格项(新格式)
temp.item.push({
...itemValue,
checked: false
})
}
})
newMyAttribute.push(temp)
})
// 初始化输入值数组
inputValues.value = Array(sourceAttribute.value.length).fill('');
// 根据 attribute 更新 myAttribute处理已选中的属性
attribute.value.forEach(attrVal => {
newMyAttribute.forEach(myAttrVal => {
if (attrVal.name === myAttrVal.name) {
attrVal.item.forEach(attrItem => {
const attrName = typeof attrItem === 'string' ? attrItem : attrItem.name;
// 查找匹配的属性项
const existingItem = myAttrVal.item.find(myAttrItem => myAttrItem.name === attrName);
if (existingItem) {
// 如果找到匹配项,标记为选中
existingItem.checked = true;
} else {
// 如果没找到,添加新项
myAttrVal.item.push({
...(typeof attrItem === 'string' ? { name: attrItem } : attrItem),
checked: true
});
}
});
}
});
});
myAttribute.value = newMyAttribute
// 因为 skuData 是实时监听 myAttribute 变化并自动生成,使用微任务确保已生成
nextTick(() => {
console.log("sku.value>>>", sku.value);
console.log(" form.skuData2>>>", form.skuData);
sku.value.forEach(skuItem => {
form.skuData.forEach(skuDataItem => {
console.log("form.skuDataItem>>>", skuDataItem);
if (skuItem.sku === skuDataItem.sku) {
structure.value.forEach(structureItem => {
skuDataItem[structureItem.name] = skuItem[structureItem.name]
})
}
})
})
isInit.value = false
})
})
}
// 获取属性的图片路径
const getAttributeImage = (attrName, attrValue) => {
if (!attrName || !attrValue) return null;
const attrGroup = myAttribute.value.find(attr => attr.name === attrName);
if (!attrGroup) return null;
const attrItem = attrGroup.item.find(item => item.name === attrValue);
return attrItem?.image || null;
}
// 初始化属性
watch(
attribute,
() => {
if (!isAsync.value) {
init()
}
},
{ immediate: true, deep: true }
)
// 监听选中属性的变化
watch(myAttribute, () => {
if (!isInit.value) {
// 更新父组件
emit('update:attribute', emitAttribute.value)
}
// 解决通过 $emit 更新后无法拿到 attribute 最新数据的问题
nextTick(() => {
if (emitAttribute.value.length !== 0) {
combinationAttribute()
} else {
form.skuData = []
const obj = {
sku: emptySku.value
}
structure.value.forEach(v => {
if (!(v.type === 'slot' && v.skuProperty === false)) {
obj[v.name] = typeof v.defaultValue !== 'undefined' ? v.defaultValue : ''
}
})
console.log('obj=', obj);
form.skuData.push(obj)
}
clearValidate()
})
}, { deep: true })
// 监听skuData变化
watch(() => form.skuData, (newValue, oldValue) => {
if (!isInit.value || (newValue.length === 1 && newValue[0].sku === emptySku.value)) {
// 如果有老数据,或者 sku 数据为空,则更新父级 sku 数据
if (oldValue.length || !sku.value.length) {
// 更新父组件
const arr = [];
newValue.forEach(v1 => {
const obj = {
sku: v1.sku,
skuData: v1.skuData || {} // 完整的SKU数据
};
structure.value.forEach(v2 => {
if (!(v2.type === 'slot' && v2.skuProperty === false)) {
obj[v2.name] = v1[v2.name] || (typeof v2.defaultValue !== 'undefined' ? v2.defaultValue : '');
}
});
arr.push(obj);
});
emit('update:sku', arr);
}
}
}, { deep: true })
// 组合属性生成SKU表格数据
const combinationAttribute = (index = 0, dataTemp = []) => {
if (index === 0) {
for (let i = 0; i < emitAttribute.value[0].item.length; i++) {
const attrItem = emitAttribute.value[0].item[i];
const attrName = attrItem.name;
const obj = {
sku: attrName,
[emitAttribute.value[0].name]: attrName,
skuData: { // 存储完整的SKU对象数据
[emitAttribute.value[0].name]: attrItem
}
};
structure.value.forEach(v => {
if (!(v.type === 'slot' && v.skuProperty === false)) {
obj[v.name] = typeof v.defaultValue !== 'undefined' ? v.defaultValue : '';
}
});
dataTemp.push(obj);
}
} else {
const temp = [];
for (let i = 0; i < dataTemp.length; i++) {
for (let j = 0; j < emitAttribute.value[index].item.length; j++) {
const attrItem = emitAttribute.value[index].item[j];
const attrName = attrItem.name;
const newItem = JSON.parse(JSON.stringify(dataTemp[i]));
// 添加新的属性名称
newItem[emitAttribute.value[index].name] = attrName;
// 更新SKU编码
newItem['sku'] = [newItem['sku'], attrName].join(separator.value);
// 更新完整的SKU对象数据
newItem.skuData = {
...newItem.skuData,
[emitAttribute.value[index].name]: attrItem
};
temp.push(newItem);
}
}
dataTemp = temp;
}
// if (index !== emitAttribute.value.length - 1) {
// combinationAttribute(index + 1, dataTemp);
// } else {
// if (!isInit.value || isAsync.value) {
// // 将原有的 sku 数据和新的 sku 数据比较,相同的 sku 则把原有的 sku 数据覆盖到新的 sku 数据里
// for (let i = 0; i < form.skuData.length; i++) {
// for (let j = 0; j < dataTemp.length; j++) {
// if (form.skuData[i].sku === dataTemp[j].sku) {
// // 保留原SKU数据中的属性值
// structure.value.forEach(structureItem => {
// dataTemp[j][structureItem.name] = form.skuData[i][structureItem.name];
// });
// // 保留原SKU数据中的完整对象信息
// dataTemp[j].skuData = {
// ...dataTemp[j].skuData,
// ...form.skuData[i].skuData
// };
// }
// }
// }
// }
// form.skuData = dataTemp;
// }
if (index !== emitAttribute.value.length - 1) {
combinationAttribute(index + 1, dataTemp);
} else {
// 先合并外部传入的 sku 数据
if (sku.value && sku.value.length) {
const skuMap = new Map();
sku.value.forEach(skuItem => {
skuMap.set(skuItem.sku, skuItem);
});
dataTemp.forEach(dataItem => {
const matched = skuMap.get(dataItem.sku);
if (matched) {
structure.value.forEach(structureItem => {
dataItem[structureItem.name] = matched[structureItem.name];
});
// 合并skuData对象
dataItem.skuData = {
...dataItem.skuData,
...matched.skuData
};
}
});
}
form.skuData = dataTemp;
}
}
// 查找属性的详细信息
const findAttributeDetail = (attrName, itemName) => {
const attrGroup = myAttribute.value.find(attr => attr.name === attrName);
if (!attrGroup) return { name: itemName };
const item = attrGroup.item.find(item => item.name === itemName);
if (!item) return { name: itemName };
// 返回完整的属性对象仅过滤掉checked状态
const { checked, ...itemDetail } = item;
return itemDetail;
}
// 添加新属性值
const onAddAttribute = (index) => {
const newValue = inputValues.value[index]?.trim();
if (!newValue) {
ElMessage.warning('请输入规格名称');
return;
}
// 检查分隔符
if (newValue.includes(separator.value)) {
ElMessage.warning(`规格里不允许出现「 ${separator.value} 」字符,请检查后重新添加`);
return;
}
// 检查重复
if (myAttribute.value[index].item.some(item => item.name === newValue)) {
ElMessage.warning('请勿添加相同规格');
return;
}
// 添加新属性,并默认选中
myAttribute.value[index].item.push({
name: newValue,
checked: true
});
// 清空输入框
inputValues.value[index] = '';
}
// 批量设置
const onBatchSet = (field) => {
if (batch[field] !== '') {
form.skuData.forEach(row => {
row[field] = batch[field]
})
batch[field] = ''
}
}
// 表单验证
const validate = (callback) => {
if (formRef.value) {
formRef.value.validate(valid => {
callback && callback(valid)
})
} else {
callback && callback(false)
}
}
// 自定义验证
const validateFieldByColumns = (columns, callback) => {
if (!formRef.value) {
callback && callback(false)
return
}
const propPaths = []
form.skuData.forEach((_, i) => {
columns.forEach(col => {
propPaths.push(`skuData.${i}.${col}`)
})
})
formRef.value.validateField(propPaths, valid => {
callback && callback(valid)
})
}
// 按行验证
const validateFieldByRows = (index, prop, callback) => {
if (formRef.value) {
formRef.value.validateField([`skuData.${index}.${prop}`], valid => {
callback && callback(valid)
})
} else {
callback && callback(false)
}
}
// 清除验证
const clearValidate = () => {
if (formRef.value) {
formRef.value.clearValidate()
}
}
// 删除规格先
const deleteAttrItemName = (attrIndex) => {
myAttribute.value.splice(attrIndex, 1)
emit('update:attribute', emitAttribute.value)
}
// 添加新属性(带图片)
const onAddAttributeWithImage = (index, name, imagePath) => {
if (!name || typeof name !== 'string') {
ElMessage.warning('请提供有效的规格名称');
return;
}
const newValue = name.trim();
if (!newValue) {
ElMessage.warning('规格名称不能为空');
return;
}
// 检查分隔符
if (newValue.includes(separator.value)) {
ElMessage.warning(`规格里不允许出现「 ${separator.value} 」字符,请检查后重新添加`);
return;
}
// 检查重复
if (myAttribute.value[index].item.some(item => item.name === newValue)) {
ElMessage.warning('请勿添加相同规格');
return;
}
// 添加新属性(带图片),并默认选中
myAttribute.value[index].item.push({
name: newValue,
image: imagePath || '',
checked: true
});
return true;
}
// 监听属性变化初始化checked数组
watch(() => myAttribute.value, () => {
// 初始化checked数组
checked.value = myAttribute.value.map(attr =>
attr.item.filter(item => item.checked).map(item => item.name)
)
}, { deep: true, immediate: true })
watch(sourceAttribute, () => {
if (!isAsync.value) {
init()
}
}, { immediate: true, deep: true })
// 处理checkbox变化
const onCheckedChange = (attrIndex, itemIndex, isChecked) => {
// 更新原始item的checked状态
myAttribute.value[attrIndex].item[itemIndex].checked = isChecked
}
// 暴露方法
defineExpose({
init,
validate,
validateFieldByColumns,
validateFieldByRows,
clearValidate,
onAddAttributeWithImage
})
</script>
<style lang="scss" scoped>
.sku-form {
&-container {
margin-bottom: 10px;
&-1 {
.sku-form-section {
margin-bottom: 10px;
}
.sku-form-title {
font-weight: bold;
}
.sku-form-tags-box {
margin-bottom: 10px;
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
}
&-2 {
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 4px;
.sku-form-section {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.sku-form-title {
min-width: 70px;
margin-right: 10px;
font-weight: bold;
}
.sku-form-tags-box {
flex: 1;
margin-right: 10px;
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.sku-form-add-tags {
width: 230px;
}
}
}
&-table {
:deep(.el-table .cell) {
padding: 0 5px;
}
:deep(.el-form-item) {
margin-bottom: 0;
}
:deep(.el-form-item__content) {
min-height: 32px;
display: flex;
align-items: center;
}
&-disabled {
.el-table {
pointer-events: none;
}
}
}
&-batch {
margin-bottom: 5px;
}
&-tips {
margin-right: 5px;
color: #409eff;
cursor: pointer;
}
}
// 新增样式 - 规格选项相关
.sku-checkbox-content {
display: flex;
align-items: center;
gap: 4px;
}
.sku-option-image {
width: 24px;
height: 24px;
object-fit: cover;
border-radius: 2px;
}
.sku-table-cell {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.sku-table-image {
width: 20px;
height: 20px;
object-fit: cover;
border-radius: 2px;
}
</style>

View File

@ -1,25 +0,0 @@
import SkuForm from './SkuForm.vue'
// 版本信息
const version = '1.0.0'
// 组件安装函数
const install = (app) => {
app.component('SkuForm', SkuForm)
}
// 组件库对象
const SkuFormLib = {
version,
install,
SkuForm
}
// Vue插件安装函数
SkuForm.install = app => {
app.component('SkuForm', SkuForm)
}
// 导出方式支持 ES Module、CommonJS 和全局变量
export { version, SkuForm }
export default SkuFormLib

View File

@ -1,307 +1,371 @@
<template>
<el-form>
<el-form-item label="销售属性" name="skuAttributes">
<div class="mb-[10px] bg-[#fafafa] p-[8px] max-w-[1000px]" v-for="(item, index) in skuAttributes">
<div class="flex items-center relative">
<!-- <a-popconfirm title="确定删除吗?" @confirm="deleteSkuAttr(index)" placement="right">
<a-button class="absolute top-[5px] right-[5px]" type="text">删除</a-button>
</a-popconfirm> -->
<div class="absolute top-[5px] right-[5px]" @confirm="deleteSkuAttr(index)">删除</div>
<div class="w-[60px] text-right">属性名称:</div>
<el-input class="ml-5 w-[130px]" v-model="item.title" placeholder="请输入属性名称"></el-input>
</div>
<div class="flex items-start mt-[10px] overflow-x-auto">
<div class="w-[60px] text-right leading-[32px]">属性值:</div>
<div class="ml-5 relative sku_item" v-for="(text, cindex) in item.values">
<!-- <DeleteIcon class="sku_item_delete absolute top-[-4px] right-[4px] z-50"
@click="deleteSkuAttrName(index, cindex)"></DeleteIcon> -->
<el-icon class="sku_item_delete absolute top-[-4px] right-[4px] z-50" color="red"
@click="deleteSkuAttrName(index, cindex)">
<CircleCloseFilled />
</el-icon>
<div class="flex flex-col items-center mr-[10px]">
<el-input placeholder="请输入属性值" class="w-[130px]" v-model="text.attributeValue"></el-input>
<div v-if="item.isAddImage"
class="relative w-[100px] h-[100px] border-solid border-[#eee] border-[1px] mt-[10px] cursor-pointer flex justify-center items-center bg-white"
@click="addSkuAttrImage(index, cindex)">
<img v-if="text.thumbnailUrl" class="w-[100%] h-[100%]" :src="text.thumbnailUrl" />
<!-- <svgIcon v-else class="w-[40px] h-[40px]" name="add" color="#eee"></svgIcon> -->
<el-icon v-else class="w-[40px] h-[40px]" name="add" color="#eee">
<Plus />
</el-icon>
<div class="ml-[80px] pr-8">
<div class="flex m-4">
<!-- :disabled="specItem.length >= 3" -->
<el-button type="primary" @click="addSpecItem">添加规格项</el-button>
<!-- <div class="ml-4 form-tips">最多支持3个规格项</div> -->
</div>
<template v-for="(item, index) in specItem" :key="index">
<del-wrap @close="handleDelSpecItem(index)">
<div class="flex p-[16px] ml-4 mt-[16px] spec-item">
<div class="flex-none mr-[10px]">
<div class="mt-2">规格名</div>
<div class="mt-6">规格值</div>
</div>
<div class="spec-item__content">
<div>
<el-input v-model="item.name" style="width: 240px" maxlength="20" show-word-limit>
</el-input>
<!-- <el-checkbox class="ml-4" :false-label="0" :true-label="1" v-model="item.has_image"
@change="addImage(index, $event)">
规格图片
</el-checkbox> -->
</div>
<div class="flex flex-wrap col-top">
<div class="mt-4 mr-4" v-for="(subItem, subIndex) in item.value" :key="subIndex">
<del-wrap @close="removeSpecValue(index, subIndex)">
<el-input class="w-40" v-model="subItem.value" maxlength="20" show-word-limit
@blur="checkValue(index, subIndex)"></el-input>
</del-wrap>
<div v-if="item.has_image">
<material-picker class="mt-4" :limit="1" size="60px" v-model="subItem.image">
</material-picker>
</div>
</div>
<div class="mt-4">
<el-button @click="addSpecValue(index)">+ 添加规格值</el-button>
</div>
</div>
</div>
<el-button :disabled="item.values.length >= 3" type="text" @click="addSkuAttrName(index)">
增加字段
</el-button>
<!-- <el-button class="ml-[10px]" v-if="isAddImg" @click="toggleSkuImg(index)">上传图片</el-button>
<el-button class="ml-[10px]" v-if="item.isAddImage" @click="toggleSkuImg(index)">
取消上传
</el-button> -->
</div>
</div>
<el-button type="primary" @click="addSkuAttr">增加销售属性</el-button>
</el-form-item>
<el-form-item label="销售规格" name="stockKeepUnits">
<el-table :data="stockKeepUnits" class="mt-[10px] max-w-[1000px] w-auto" border>
<!-- 销售规格 -->
<el-table-column prop="attributeValue" label="销售规格">
<template #default="{ row }">
<span>{{ row.attributeValue }}</span>
</template>
</el-table-column>
<!-- 售价 -->
<el-table-column prop="price" label="*售价">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.price" />
</template>
</el-table-column>
<!-- 市场价 -->
<el-table-column prop="marketPrice" label="*市场价">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.marketPrice" />
</template>
</el-table-column>
<!-- 库存 -->
<el-table-column prop="stock" label="*库存">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.stock" />
</template>
</el-table-column>
</el-table>
<!-- <el-table class="mt-[10px] max-w-[1000px] w-auto" bordered :data="stockKeepUnits">
<template #bodyCell="{ column, record, index }">
<div v-if="column.dataIndex === 'attributeValue'">
<span>{{ record.attributeValue }}</span>
</div>
<div v-else-if="column.dataIndex === 'thumbnailUrl'">
<img v-if="record.thumbnailUrl" class="w-[90px] h-[90px]" :src="record.thumbnailUrl" />
</div>
<div v-else>
<el-input class="w-[80px]" v-model:value="record[column.dataIndex]"></el-input>
</del-wrap>
</template>
</div>
<el-form-item label="规格明细" class="mt-8">
<div class="flex pl-[10px] mb-4">
<popover-input class="mr-2" :disabled="disabledBatchBtn" @confirm="batchSetting($event, 'price')">
<el-button :disabled="disabledBatchBtn">设置价格</el-button>
</popover-input>
<!-- <popover-input class="mr-2" :disabled="disabledBatchBtn" @confirm="batchSetting($event, 'line_price')">
<el-button :disabled="disabledBatchBtn">设置划线价</el-button>
</popover-input> -->
<popover-input class="mr-2" :disabled="disabledBatchBtn" @confirm="batchSetting($event, 'cost_price')">
<el-button :disabled="disabledBatchBtn">设置成本价</el-button>
</popover-input>
<popover-input class="mr-2" :disabled="disabledBatchBtn" @confirm="batchSetting($event, 'stock')">
<el-button :disabled="disabledBatchBtn">设置库存</el-button>
</popover-input>
<!-- <popover-input class="mr-2" :disabled="disabledBatchBtn" @confirm="batchSetting($event, 'volume')">
<el-button :disabled="disabledBatchBtn">设置体积</el-button>
</popover-input>
<popover-input class="mr-2" :disabled="disabledBatchBtn" @confirm="batchSetting($event, 'weight')">
<el-button :disabled="disabledBatchBtn">设置重量</el-button>
</popover-input>
<popover-input :disabled="disabledBatchBtn" @confirm="batchSetting($event, 'code')">
<el-button :disabled="disabledBatchBtn">设置条码</el-button>
</popover-input> -->
</div>
<el-table class="pl-[10px]" :data="specParams.tableData" max-height="600" :row-height="75" tooltip-effect="dark"
:border="false" big-data-checkbox @selection-change="selectDataChange">
<el-table-column type="selection" width="55" />
<el-table-column v-for="(item, index) in specItem" :key="index" :label="item.name" min-width="100"
:show-overflow-tooltip="true">
<template #default="{ row }">
{{ row.sku_value_arr[index] }}
</template>
</el-table-column>
<!-- <el-table-column label="规格图片" min-width="90">
<template #default="{ row, $index }">
<del-wrap @close="removeSpecImage($index)" v-if="row.image">
<el-image style="width: 50px; height: 50px" :src="row.image" @click="addSpecImage($index)">
</el-image>
</del-wrap>
<div class="flex items-center justify-center spec-image" @click="addSpecImage($index)" v-else>
<el-icon>
<Plus />
</el-icon>
</div>
</template>
</el-table> -->
</el-form-item>
</el-form>
</el-table-column> -->
<el-table-column min-width="100">
<template #header>
<span class="require-text">*</span> 价格
</template>
<template #default="{ row }">
<el-input class="spec-input" type="number" v-model="row.price"></el-input>
</template>
</el-table-column>
<!-- <el-table-column min-width="100">
<template #header> 划线价 </template>
<template #default="{ row }">
<el-input class="spec-input" type="number" v-model="row.line_price"></el-input>
</template>
</el-table-column> -->
<el-table-column label="成本价" min-width="100">
<template #header>
<span class="require-text">*</span> 成本价
</template>
<template #default="{ row }">
<el-input class="spec-input" type="number" v-model="row.cost_price"></el-input>
</template>
</el-table-column>
<el-table-column min-width="100">
<template #header>
<span class="require-text">*</span> 库存
</template>
<template #default="{ row }">
<el-input class="spec-input" type="number" v-model="row.stock"></el-input>
</template>
</el-table-column>
<!-- <el-table-column min-width="100">
<template #header> 体积 </template>
<template #default="{ row }">
<el-input class="spec-input" type="number" v-model="row.volume"></el-input>
</template>
</el-table-column>
<el-table-column label="重量" min-width="100">
<template #header> 重量 </template>
<template #default="{ row }">
<el-input class="spec-input" type="number" v-model="row.weight"></el-input>
</template>
</el-table-column>
<el-table-column label="条码" min-width="100">
<template #default="{ row }">
<el-input class="spec-input" type="number" v-model="row.code"></el-input>
</template>
</el-table-column> -->
</el-table>
</el-form-item>
<material-picker ref="materialRef" :hiddenUpload="true" @change="changeSpecImage" />
</template>
<script setup lang="ts">
import { ref, watch, type Ref, computed } from 'vue'
import type { skuType, skuAttrItemType } from './type.d'
import type { TableColumnCtx } from 'element-plus'
import { deepClone } from '@yipai-front-end/lib'
import { ElMessage } from 'element-plus'
<script lang="ts" setup>
import feedback from "@/utils/feedback";
import { flatten } from "@/utils/util";
import type { SkuItemList, SkuNameList } from "@/api/goods"
const skuAttributes: Ref<skuAttrItemType[]> = ref([])
const stockKeepUnits: Ref<skuType[]> = ref([])
let afterSku: skuType[] = []
const emit = defineEmits(['update:attribute'])
const skuAttrItem: skuAttrItemType = {
title: '',
isAddImage: false,
values: [{ thumbnailUrl: '', attributeValue: '' }],
}
const columns = [
const props = withDefaults(
defineProps<{
modelValue: any;
}>(),
{
dataIndex: 'attributeValue',
title: '销售规格',
},
{
dataIndex: 'thumbnailUrl',
title: '图片',
},
{
dataIndex: 'price',
title: '*售价',
},
{
dataIndex: 'marketPrice',
title: '*市场价',
},
{
dataIndex: 'stock',
title: '*库存',
},
{
dataIndex: 'specificationBarCode',
title: '商品条码',
},
]
modelValue: {},
}
);
// 是否可以增加图片
const isAddImg = computed(() => {
return skuAttributes.value.findIndex((e) => e.isAddImage == true) == -1 ? true : false
})
const materialRef = shallowRef();
const specParams = reactive<any>({
tableData: [],
selectData: [],
tableDataIndex: 0 as number,
});
// 监听sku本身的变化,并将当前sku进行备份
watch(
() => stockKeepUnits.value,
(value) => {
afterSku = deepClone(value)
},
{ deep: true }
)
const disabledBatchBtn = computed(() => !specParams.selectData.length);
// 监听销售属性的变化,并构建sku
watch(
() => skuAttributes.value,
(value) => {
if (value.length) {
generateSku(deepClone(value))
const specItem = computed(() => props.modelValue.sku_name_list || []);
const specSubItem = computed(() => props.modelValue.sku_list || []);
// 新增规格项
const addSpecItem = () => {
const len = props.modelValue.sku_name_list.length;
// if (len >= 3) return feedback.msgError("最多添加3个规格项");
props.modelValue.sku_name_list.push({
has_image: 0,
id: "",
name: "",
value: [
{
value: "",
image: "",
},
],
spec_id: 0
});
};
// 删除规格项
const handleDelSpecItem = (index: number) => {
const len = props.modelValue.sku_name_list.length;
if (len <= 1) return feedback.msgError("至少一个规格项");
props.modelValue.sku_name_list.splice(index, 1);
};
// 新增规格值
const addSpecValue = (index: number) => {
props.modelValue.sku_name_list[index].value.push({
// id: "",
value: "",
image: "",
});
};
// 删除规格值
const removeSpecValue = (index: number, subIndex: number) => {
props.modelValue.sku_name_list[index].value.splice(subIndex, 1);
};
const addImage = (i: number, v: any) => {
const skuItem: SkuNameList[] = props.modelValue.sku_name_list;
const skuSubItem: SkuItemList[] = props.modelValue.sku_list;
skuItem.forEach((item, index: number) => {
item.has_image = 0;
if (i == index) {
item.has_image = v;
}
});
skuSubItem.forEach(sitem => {
sitem.image = "";
});
specParams.tableData.forEach((item: { image: string; }) => {
item.image = "";
});
};
// 娇艳规格值
const checkValue = (index: number, subIndex: number) => {
const skuItem = props.modelValue?.sku_name_list[index];
const value = skuItem?.value[subIndex].value;
const res = skuItem?.value.filter(
(item: { value: string }) =>
item.value == value && value != "" && value.length != 0
);
const lessTops = res.length >= 2;
if (lessTops) {
feedback.msgWarning("已存在相同规格值");
skuItem.value[subIndex].value = "";
}
};
const selectDataChange = (value: SkuItemList[]) => {
specParams.selectData = value.map(item => item.ids);
};
const batchSetting = (value: string, fields: string | never) => {
specParams.tableData.forEach((item: { [x: string]: string; ids: any; }) => {
if (specParams.selectData.includes(item.ids)) {
item[fields] != undefined && (item[fields] = value);
}
});
};
//设置字段名称
const setFields = (prev: any, next: any) => {
let valueArr = [prev, next]
valueArr = valueArr.filter(item => item.value !== undefined)
const ids = flatten(valueArr.map(item => item.ids)).join()
const value = flatten(valueArr.map(item => item.value))
return {
id: prev.id ? prev.id : '',
ids: ids,
value,
sku_value_arr: value,
// image: prev.image ? prev.image : next.image,
price: prev.price ? prev.price : '',
// line_price: prev.line_price ? prev.line_price : '',
cost_price: prev.cost_price ? prev.cost_price : '',
stock: prev.stock ? prev.stock : '',
// volume: prev.volume ? prev.volume : '',
// weight: prev.weight ? prev.weight : '',
// code: prev.code ? prev.code : ''
spec_value_ids: prev.spec_value_ids ? prev.spec_value_ids : 0,
item_id: prev.item_id ? prev.item_id : '',
}
};
// 通过规格项和规格值得到一个表格data
const getTableData = (arr: any[]) => {
arr = JSON.parse(JSON.stringify(arr))
return arr.reduce(
(prev, next) => {
const newArr = []
for (let i = 0; i < prev.length; i++) {
if (!next.length) {
newArr.push(setFields(prev[i], {}))
}
for (let j = 0; j < next.length; j++) {
next[j].ids = j
newArr.push(setFields(prev[i], next[j]))
}
}
return newArr
},
[{}]
)
}
const setTableData = () => {
const skuNameList = props.modelValue.sku_name_list;
const tableData = specParams.tableData;
const specList = skuNameList.map((item: SkuNameList) => item.value)
const newData = getTableData(specList);
const rawData = JSON.parse(JSON.stringify(tableData));
const rawObject: any = {};
rawData.forEach((item: any) => {
if (item.sku_value_arr !== undefined) {
rawObject[item.sku_value_arr] = item;
}
})
specParams.tableData = newData.map((item: any) =>
rawObject[item.sku_value_arr]
? {
...rawObject[item.sku_value_arr],
value: item.value,
ids: item.ids,
image: item.image || rawObject[item.sku_value_arr].image,
}
: item
);
};
watch(
() => specItem.value,
() => {
setTableData();
},
{ deep: true }
)
{ deep: true, immediate: true }
);
/**
* 更新销售属性构建sku
* @param skuAttribute
*/
function generateSku(skuAttribute: skuAttrItemType[]) {
let attrValue: any[] = []
skuAttribute.map((item) => {
attrValue.push(item.values)
})
let skus: any[] = []
if (attrValue.length === 0) {
stockKeepUnits.value = []
return
watch(
() => props.modelValue.sku_list,
(value) => {
specParams.tableData = value
}
);
skus = attrValue.reduce((col: any[], set) => {
let res: any[] = []
col.forEach((c) => {
set.forEach((s) => {
let t = c.attributeValue + ',' + s.attributeValue
res.push({ attributeValue: t, thumbnailUrl: c.thumbnailUrl || s.thumbnailUrl || '' })
})
})
return res
})
// 增加,回显相关字段
skus.map((e: skuType) => {
// 寻找销售规格一致的副本数据
let old = afterSku.find((item) => item.attributeValue == e.attributeValue)
console.log('单项', e)
e.id = old == null ? '' : old.id
e.price = old == null ? '' : old.price
e.marketPrice = old == null ? '' : old.marketPrice
e.stock = old == null ? '' : old.stock
e.specificationBarCode = old == null ? '' : old.specificationBarCode
return e
})
console.log(skus, 'skus')
stockKeepUnits.value = skus
}
watch(
() => specParams.tableData,
(value) => {
props.modelValue.sku_list = value
},
{ deep: true, immediate: true }
);
/**
* 删除销售属性
* @param index
*/
function deleteSkuAttr(index) {
skuAttributes.value.splice(index, 1)
}
/**
* 删除销售属性字段
* @param index
* @param cindex
*/
function deleteSkuAttrName(index: number, cindex: number) {
skuAttributes.value[index].values.splice(cindex, 1)
}
/**
* 增加sku属性图片
* @param index
* @param cindex
*/
async function addSkuAttrImage(index: number, cindex: number) {
let res = await chooseToFile()
console.log(res)
// 生产环境此处应该是上传到服务端,获取线上url
// 此处写法仅限测试,上传大图片可能造成卡顿
_blobToDataUrl(res[0], (res) => {
skuAttributes.value[index].values[cindex].thumbnailUrl = res
})
}
/**
* 增加销售属性字段
* @param index
*/
function addSkuAttrName(index: number) {
skuAttributes.value[index].values.push({ attributeValue: '', thumbnailUrl: '' })
}
/**
* 切换首个sku是否上传图片的状态
*/
function toggleSkuImg(index) {
let { isAddImage } = skuAttributes.value[index]
if (isAddImage) {
skuAttributes.value[index].values.map((e) => {
e.thumbnailUrl = ''
return e
})
}
skuAttributes.value[index].isAddImage = !isAddImage
}
/**
* 增加销售属性
*/
function addSkuAttr() {
skuAttributes.value.push(deepClone(skuAttrItem))
}
/**
* 动态更新表格字段
* @param index
* @param dataIndex
*/
function changeSkuData(index: number, dataIndex: string, evt: any) {
let value = evt.target.value
stockKeepUnits.value[index][dataIndex] = value
}
function save() {
message.success('请查看控制台输出')
console.log('销售属性:', skuAttributes.value)
console.log('sku:', stockKeepUnits.value)
}
function _blobToDataUrl(file, callback) {
const reader = new FileReader()
reader.onload = () => {
const url = URL.createObjectURL(file) // 获取临时访问链接
callback(url)
}
reader.readAsDataURL(file)
}
defineExpose({
skuAttributes,
stockKeepUnits
})
const addSpecImage = (index: number) => {
specParams.tableDataIndex = index;
materialRef.value?.showPopup();
};
const changeSpecImage = (value: string) => {
specParams.tableData[specParams.tableDataIndex].image = value;
};
const removeSpecImage = (index: number) => {
specParams.tableData[index].image = "";
};
</script>
<style lang="scss" scoped>
.sku_item {
&:hover {
.sku_item_delete {
opacity: 1;
}
}
<style>
.spec-item {
transition: all 1s;
background-color: var(--el-color-primary-light-9);
}
.sku_item_delete {
opacity: 0;
}
.spec-image {
width: 50px;
height: 50px;
cursor: pointer;
border: 1px dashed #e5e5e5;
}
</style>

View File

@ -1,39 +0,0 @@
/**
* sku表格字段
*/
export type skuType = {
attributeValue: string
id?: string
marketPrice: string
price: string
specificationBarCode: string
stock: string
thumbnailUrl: string
}
/**
* 销售属性类型
*/
export type skuAttrItemType = {
/**
* 是否上传图片
*/
isAddImage: boolean
/**
* 名称
*/
title: string
/**
* 具体数据
*/
values: {
/**
* 属性图片
* */
thumbnailUrl?: string
/**
* 属性名称
*/
attributeValue: string
}[]
}

View File

@ -1,591 +0,0 @@
<template>
<div class="edit-popup">
<popup ref="popupRef" :title="popupTitle" :async="true" width="550px" @confirm="handleSubmit"
@close="handleClose">
<el-form ref="formRef" :model="formData" label-width="90px" :rules="formRules">
<el-tabs type="border-card">
<el-tab-pane label="基础设置">
<el-form-item label="商品名称" prop="name" required>
<el-input v-model="formData.name" clearable placeholder="请输入商品名称" />
</el-form-item>
<el-form-item label="商品编码" prop="code">
<el-input v-model="formData.code" clearable placeholder="请输入商品编码" />
</el-form-item>
<el-form-item label="商品分类" prop="first_category_id" required>
<div class="flex w-full">
<div class="w-4/12 mr-1">
<el-select v-model="formData.first_category_id" placeholder="请选择分类"
@change="selectFirstCategory">
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in firstCategory" />
</el-select>
</div>
<div class="w-4/12 mr-1">
<el-select v-model="formData.second_category_id" placeholder="请选择分类"
@change="selectSecondCategory">
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in secondCategory" />
</el-select>
</div>
<div class="w-4/12">
<el-select v-model="formData.third_category_id" placeholder="请选择分类">
<el-option :label="item.name" :value="item.id" :key="item.id"
v-for="item in thirdCategory" />
</el-select>
</div>
</div>
</el-form-item>
<el-form-item label="商品卖点" prop="remark">
<el-input v-model="formData.remark" clearable placeholder="请输入商品卖点" />
</el-form-item>
<el-form-item label="商品主图" prop="image" required>
<material-picker v-model="formData.image" :limit="1" />
</el-form-item>
<el-form-item label="商品轮播图" prop="goods_image" required>
<material-picker v-model="formData.goods_image" :limit="8" />
</el-form-item>
</el-tab-pane>
<el-tab-pane label="价格库存">
<el-radio-group v-model="formData.spec_type">
<el-radio :value="1">统一规格</el-radio>
<el-radio :value="2">多规格</el-radio>
</el-radio-group>
<!-- 单规格 -->
<div v-if="formData.spec_type === 1">
<el-table :data="[{}]" style="width: 100%">
<el-table-column label="价格(元)" prop="one_price">
<template #default="scope">
<el-input v-model="formData.one_price" clearable placeholder="请输入价格(元)" />
</template>
</el-table-column>
<el-table-column label="成本价(元)" prop="one_cost_price">
<template #default="scope">
<el-input v-model="formData.one_cost_price" clearable placeholder="请输入成本价(元)" />
</template>
</el-table-column>
<el-table-column label="请输入库存" prop="one_stock">
<template #default="scope">
<el-input v-model="formData.one_stock" clearable placeholder="请输入库存" />
</template>
</el-table-column>
</el-table>
</div>
<!-- 多规格 -->
<div v-if="formData.spec_type === 2">
<div v-for="(spec, specIdx) in specs" :key="specIdx" class="mb-2">
<el-form-item :label="'规格名'">
<el-input v-model="spec.name" placeholder="如口味/尺寸" style="width: 100px;"
@input="onSpecNameChange" />
</el-form-item>
<el-form-item :label="'规格值'" style="flex:1;">
<el-input v-model="spec.valuesStr" type="textarea" :rows="2" placeholder="每行一个规格值"
@change="onSpecValueChange(specIdx)" />
</el-form-item>
<div class="flex justify-end">
<el-button type="danger" @click="removeSpec(specIdx)">删除</el-button>
</div>
</div>
<el-button type="primary" @click="addSpec" :disabled="specs.length >= 3"
class="mb-2">添加规格项</el-button>
<el-table :data="skuTable" style="width: 100%; margin-top: 10px;">
<el-table-column v-for="(spec, idx) in specs" :key="idx"
:label="spec.name || `规格${idx + 1}`" :prop="'spec' + idx" />
<el-table-column label="价格(元)">
<template #default="scope">
<el-input v-model="scope.row.price" placeholder="价格" />
</template>
</el-table-column>
<el-table-column label="成本价(元)">
<template #default="scope">
<el-input v-model="scope.row.cost_price" placeholder="成本价" />
</template>
</el-table-column>
<el-table-column label="库存">
<template #default="scope">
<el-input v-model="scope.row.stock" placeholder="库存" />
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="商品详情">
<editor v-model="formData.content" :height="667" />
</el-tab-pane>
<el-tab-pane label="销售设置">
<el-form-item label="虚拟销量" prop="virtual_sales_sum">
<el-input v-model="formData.virtual_sales_sum" placeholder="请输入虚拟销量" />
</el-form-item>
<el-form-item label="虚拟浏览量" prop="virtual_click">
<el-input v-model="formData.virtual_click" placeholder="请输入虚拟浏览量" />
</el-form-item>
<el-form-item label="库存显示" prop="is_show_stock">
<el-radio-group v-model="formData.is_show_stock">
<el-radio :value="1">显示</el-radio>
<el-radio :value="0">不显示</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="库存预警" prop="stock_warn">
<el-input v-model="formData.stock_warn" placeholder="请输入库存预警" />
<span>设置最低库存预警值当库存低于预警值时会出现在库存预警商品列表页0为不预警。</span>
</el-form-item> -->
<!-- <el-form-item label="配送方式" prop="goods_image" required>
<el-checkbox v-model="formData.is_express" label="快递" size="large" />
<el-checkbox v-model="formData.is_selffetch" label="门店自提" size="large" />
</el-form-item> -->
<!-- <el-form-item label="积分抵扣" prop="is_integral">
<el-radio-group v-model="formData.is_integral">
<el-radio value="1">允许积分抵扣</el-radio>
<el-radio value="0">不能使用积分抵扣</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="赠送积分" prop="give_integral_type">
<el-radio-group v-model="formData.give_integral_type">
<el-radio :value="1">
赠送固定积分
<el-input v-model="formData.give_integral_num" style="width: 120px"
placeholder="请输入赠送积分" />
</el-radio>
<el-radio :value="2" class="mt-1">
按比例赠送积分
<el-input v-model="formData.give_integral_ratio" style="width: 120px"
placeholder="请输入赠送积分比例" />
<span class="ml-1">%</span>
</el-radio>
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="会员价" prop="is_member">
<el-radio-group v-model="formData.is_member">
<el-radio value="0">不参与会员价</el-radio>
<el-radio value="1">根据会员等级折扣计算会员价</el-radio>
</el-radio-group>
</el-form-item> -->
<el-form-item label="销售状态" prop="status" required>
<el-radio-group v-model="formData.status">
<el-radio :value="1">立即上架</el-radio>
<el-radio :value="0">放入仓库</el-radio>
</el-radio-group>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
</popup>
</div>
</template>
<script lang="ts" setup name="goodsEdit">
import type { FormInstance } from 'element-plus'
import Popup from '@/components/popup/index.vue'
import { apiGoodsAdd, apiGoodsEdit, apiGoodsDetail, checkCategory } from '@/api/goods'
import type { PropType } from 'vue'
import feedback from '@/utils/feedback'
import type { AnyAaaaRecord } from 'dns'
defineProps({
dictData: {
type: Object as PropType<Record<string, any[]>>,
default: () => ({})
}
})
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const mode = ref('add')
const goodsSpec = ref(2)
// 弹窗标题
const popupTitle = computed(() => {
return mode.value == 'edit' ? '编辑商品主表' : '新增商品主表'
})
// 表单数据
const formData = reactive({
id: '',
name: '',
code: '',
first_category_id: '',
second_category_id: '',
third_category_id: '',
remark: '',
image: '',
goods_image: '',
video: '',
poster: '',
one_price: '',
one_cost_price: '',
one_stock: '',
one_market_price: '',
one_spec_image: '',
one_volume: '',
one_weight: '',
one_bar_code: '',
spec_name: '',
spec_type: 1,
content: '',
sales_sum: 0,
click_count: 0,
virtual_sales_sum: '',
virtual_click: '',
stock_warn: 0,
is_show_stock: 1,
is_express: 1,
is_integral: 0,
give_integral_type: 0,
give_integral_num: '',
give_integral_ratio: '',
status: 0,
brand_id: '',
supplier_id: '',
sort: '',
max_price: 0,
min_price: 0,
market_price: 0,
free_shipping_type: 0,
free_shipping: 0,
free_shipping_template_id: 0,
is_commission: 0,
first_ratio: 0,
second_ratio: 0,
three_ratio: 0,
is_share_bouns: 0,
region_ratio: 0,
shareholder_ratio: 0,
is_new: '',
is_best: '',
is_like: '',
is_team: 0,
is_member: 0,
give_integral: 0,
is_selffetch: '',
})
// // 表单验证
const formRules = reactive<any>({
name: [{
required: true,
message: '请输入商品名称',
trigger: ['blur']
}],
first_category_id: [{
required: true,
message: '请选择商品分类',
trigger: ['blur']
}],
image: [{
required: true,
message: '请上传商品主图',
trigger: ['blur']
}],
goods_image: [{
required: true,
message: '请上传商品轮播图',
trigger: ['blur']
}],
})
onMounted(() => {
getCategory()
});
// 获取详情
const setFormData = async (data: Record<any, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key]
}
}
}
const getDetail = async (row: Record<string, any>) => {
const data = await apiGoodsDetail({
id: row.id
})
// 提取 abs_image 并重新赋值给 goods_image
if (Array.isArray(data['base']['goods_image'])) {
data['base']['goods_image'] = data['base']['goods_image'].map(item => item.abs_image)
}
setFormData(data['base'])
// 编辑--重新组成数据
const { base, item, spec } = data
if (base.spec_type == 1) {
// 单规格
formData['one_price'] = item[0].price
formData['one_cost_price'] = item[0].cost_price
formData['one_stock'] = item[0].stock
formData['one_volume'] = item[0].volume
formData['one_weight'] = item[0].weight
formData['one_bar_code'] = item[0].bar_code
} else if (base.spec_type == 2) {
// 多规格
// 1. 渲染规格名和规格值
specs.value = spec.map((s: any) => ({
name: s.name,
valuesStr: s.values.map((v: any) => v.value).join('\n'),
values: s.values.map((v: any) => v.value)
}))
// 2. 渲染规格表格
skuTable.value = item.map((it: any) => {
const row: any = {
price: it.price,
cost_price: it.cost_price,
stock: it.stock
}
// 动态添加 spec0, spec1, spec2
if (it.spec_value_str) {
it.spec_value_str.split(',').forEach((v: string, idx: number) => {
row[`spec${idx}`] = v
})
}
return row
})
syncSpecToFormData()
}
// 设置分类
selectFirstCategory(data.first_category_id)
selectSecondCategory(data.second_category_id)
}
//
let goodsCategory = reactive<any[]>([])
let firstCategory = reactive<any[]>([])
let secondCategory = reactive<any[]>([])
let thirdCategory = reactive<any[]>([])
const getCategory = async () => {
const res = await checkCategory()
goodsCategory = res
firstCategory = res.filter((item: { pid: number }) => {
if (item.pid === 0) {
return item
}
})
}
// 选择第一个分类组,渲染第二个分组
const selectFirstCategory = (id: number) => {
secondCategory = goodsCategory.filter((item: { pid: number }) => {
if (item.pid === id) {
return item
}
})
if (secondCategory.length === 0) {
formData.second_category_id = ''
formData.third_category_id = ''
}
}
// 选择第二个分类组,渲染第三个分组
const selectSecondCategory = (id: number) => {
thirdCategory = goodsCategory.filter((item: { pid: number }) => {
if (item.pid === id) {
return item
}
})
if (thirdCategory.length === 0) {
formData.third_category_id = ''
}
}
// 提交按钮
const handleSubmit = async () => {
await formRef.value?.validate()
const data = { ...formData }
if (data.spec_type == 1 && !data.one_price && !data.one_cost_price && !data.one_stock) {
feedback.notifyError('请填写商品规格')
return false
}
data.price = skuTable.value.map(item => item.price)
data.cost_price = skuTable.value.map(item => item.cost_price)
data.stock = skuTable.value.map(item => item.stock)
data.spec_value_str = skuTable.value.map(item => {
const arr = []
if ('spec0' in item) arr.push(item.spec0)
if ('spec1' in item) arr.push(item.spec1)
if ('spec2' in item) arr.push(item.spec2)
return arr.join(',')
})
if (data.spec_type == 2 && !data.price && !data.cost_price && !data.stock && !data.spec_value_str) {
feedback.notifyError('请填写商品多规格字段')
return false
}
mode.value == 'edit'
? await apiGoodsEdit(data)
: await apiGoodsAdd(data)
popupRef.value?.close()
emit('success')
}
//打开弹窗
const open = (type = 'add') => {
mode.value = type
popupRef.value?.open()
}
// 关闭回调
const handleClose = () => {
emit('close')
}
// 多规格相关
// const specs = ref([
// { name: '', valuesStr: '', values: [] }
// ])
const specs = ref([])
const skuTable = ref<any[]>([])
const onSpecNameChange = () => {
syncSpecToFormData()
}
// 添加规格项最多3个
const addSpec = () => {
if (specs.value.length < 3) {
specs.value.push({ name: '', valuesStr: '', values: [] })
syncSpecToFormData()
}
}
// 删除规格项
const removeSpec = (idx: number) => {
specs.value.splice(idx, 1)
updateSkuTable()
syncSpecToFormData()
}
// 规格值变更
const onSpecValueChange = (idx: number) => {
const spec = specs.value[idx]
spec.values = spec.valuesStr
.split('\n')
.map(v => v.trim())
.filter(v => v)
updateSkuTable()
syncSpecToFormData()
}
// 计算所有规格组合(笛卡尔积)
function cartesianProduct(arr: any[][]) {
if (arr.length === 0) return []
return arr.reduce((a, b) =>
a.flatMap(d => b.map(e => [].concat(d, e)))
)
}
// 同步规格数据到formData
const syncSpecToFormData = () => {
formData.spec_name = specs.value.map(item => item.name)
formData.spec_values = specs.value.map(item => item.valuesStr.replace(/\n/g, ','))
}
// 更新SKU表格
const updateSkuTable = () => {
const valueArr = specs.value.map(s => s.values)
if (valueArr.some(arr => arr.length === 0)) {
skuTable.value = []
return
}
let result: any[] = []
if (valueArr.length === 1) {
result = valueArr[0].map(v1 => {
const old = skuTable.value.find(row => row.spec0 === v1)
return {
spec0: v1,
price: old ? old.price : '',
cost_price: old ? old.cost_price : '',
stock: old ? old.stock : ''
}
})
} else if (valueArr.length === 2) {
result = cartesianProduct(valueArr).map(([v1, v2]) => {
const old = skuTable.value.find(row => row.spec0 === v1 && row.spec1 === v2)
return {
spec0: v1,
spec1: v2,
price: old ? old.price : '',
cost_price: old ? old.cost_price : '',
stock: old ? old.stock : ''
}
})
} else if (valueArr.length === 3) {
result = cartesianProduct(valueArr).map(([v1, v2, v3]) => {
const old = skuTable.value.find(row => row.spec0 === v1 && row.spec1 === v2 && row.spec2 === v3)
return {
spec0: v1,
spec1: v2,
spec2: v3,
price: old ? old.price : '',
cost_price: old ? old.cost_price : '',
stock: old ? old.stock : ''
}
})
}
// 合并已有 skuTable 的价格等数据
result.forEach(row => {
// 1. 精确匹配
let old = skuTable.value.find(oldRow =>
['spec0', 'spec1', 'spec2'].every(key => row[key] === oldRow[key])
)
// 2. 如果精确找不到,尝试模糊匹配(只要有一项不同也算)
if (!old) {
old = skuTable.value.find(oldRow => {
let sameCount = 0
let total = 0
for (const key of ['spec0', 'spec1', 'spec2']) {
if (row[key] !== undefined && oldRow[key] !== undefined) {
total++
if (row[key] === oldRow[key]) sameCount++
}
}
// 至少有一项相同(比如只改了一个规格值)
return total > 0 && sameCount === total - 1
})
}
if (old) {
row.price = old.price
row.cost_price = old.cost_price
row.stock = old.stock
}
})
skuTable.value = result
}
// 监听规格变化自动生成表格
watch(specs, updateSkuTable, { deep: true })
defineExpose({
open,
setFormData,
getDetail,
selectFirstCategory,
selectSecondCategory,
mode: 'default', // 或 'simple'
})
</script>

View File

@ -78,146 +78,7 @@
</div>
<!-- 多规格 -->
<div v-if="formData.spec_type === 2">
<el-form-item label="销售属性" name="skuAttributes">
<div class="mb-[10px] bg-[#fafafa] p-[8px] max-w-[1000px]"
v-for="(item, index) in skuAttributes">
<div class="flex items-center relative">
<!-- <a-popconfirm title="确定删除吗?" @confirm="deleteSkuAttr(index)" placement="right">
<a-button class="absolute top-[5px] right-[5px]" type="text">删除</a-button>
</a-popconfirm> -->
<div class="absolute top-[5px] right-[5px]" @confirm="deleteSkuAttr(index)">删除
</div>
<div class="w-[60px] text-right">属性名称:</div>
<el-input class="ml-5 w-[130px]" v-model="item.title"
placeholder="请输入属性名称"></el-input>
</div>
<div class="flex items-start mt-[10px] overflow-x-auto">
<div class="w-[60px] text-right leading-[32px]">属性值:</div>
<div class="ml-5 relative sku_item" v-for="(text, cindex) in item.values">
<!-- <DeleteIcon class="sku_item_delete absolute top-[-4px] right-[4px] z-50"
@click="deleteSkuAttrName(index, cindex)"></DeleteIcon> -->
<el-icon class="sku_item_delete absolute top-[-4px] right-[4px] z-50"
color="red" @click="deleteSkuAttrName(index, cindex)">
<CircleCloseFilled />
</el-icon>
<div class="flex flex-col items-center mr-[10px]">
<el-input placeholder="请输入属性值" class="w-[130px]"
v-model="text.attributeValue"></el-input>
<div v-if="item.isAddImage"
class="relative w-[100px] h-[100px] border-solid border-[#eee] border-[1px] mt-[10px] cursor-pointer flex justify-center items-center bg-white"
@click="addSkuAttrImage(index, cindex)">
<img v-if="text.thumbnailUrl" class="w-[100%] h-[100%]"
:src="text.thumbnailUrl" />
<!-- <svgIcon v-else class="w-[40px] h-[40px]" name="add" color="#eee"></svgIcon> -->
<el-icon v-else class="w-[40px] h-[40px]" name="add" color="#eee">
<Plus />
</el-icon>
</div>
</div>
</div>
<el-button :disabled="item.values.length >= 3" type="text"
@click="addSkuAttrName(index)">
增加字段
</el-button>
<!-- <el-button class="ml-[10px]" v-if="isAddImg" @click="toggleSkuImg(index)">上传图片</el-button>
<el-button class="ml-[10px]" v-if="item.isAddImage" @click="toggleSkuImg(index)">
取消上传
</el-button> -->
</div>
</div>
<el-button type="primary" @click="addSkuAttr">增加销售属性</el-button>
</el-form-item>
<el-form-item label="销售规格" name="stockKeepUnits">
{{ stockKeepUnits }}
<el-table :data="stockKeepUnits" class="mt-[10px] max-w-[1000px] w-auto" border>
<!-- 销售规格 -->
<el-table-column prop="attributeValue" label="销售规格">
<template #default="{ row }">
<span>{{ row.attributeValue }}</span>
</template>
</el-table-column>
<!-- 售价 -->
<el-table-column prop="price" label="*售价">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.price" />
</template>
</el-table-column>
<!-- 市场价 -->
<el-table-column prop="marketPrice" label="*市场价">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.marketPrice" />
</template>
</el-table-column>
<!-- 库存 -->
<el-table-column prop="stock" label="*库存">
<template #default="{ row }">
<el-input class="w-[80px]" v-model="row.stock" />
</template>
</el-table-column>
</el-table>
<!-- <el-table class="mt-[10px] max-w-[1000px] w-auto" bordered :data="stockKeepUnits">
<template #bodyCell="{ column, record, index }">
<div v-if="column.dataIndex === 'attributeValue'">
<span>{{ record.attributeValue }}</span>
</div>
<div v-else-if="column.dataIndex === 'thumbnailUrl'">
<img v-if="record.thumbnailUrl" class="w-[90px] h-[90px]" :src="record.thumbnailUrl" />
</div>
<div v-else>
<el-input class="w-[80px]" v-model:value="record[column.dataIndex]"></el-input>
</div>
</template>
</el-table> -->
</el-form-item>
<!-- <div>
<div>
<el-input v-model="attributes.name" placeholder="添加规格项: 如口味/尺寸" />
</div>
<div class="mt-2">
<el-button type="primary" @click="addSourceAttribute" :disabled="specs.length >= 3"
class="mb-2">添加规格项</el-button>
<span style="color: #999;margin: 10px;">备注:商品规格只能添加三个</span>
</div>
</div> -->
<!-- <el-button type="primary" @click="addSourceAttribute" :disabled="specs.length >= 3"
class="mb-2">添加规格项</el-button> -->
<!-- <div v-for="(spec, specIdx) in specs" :key="specIdx" class="mb-2">
<el-form-item :label="'规格名'">
<el-input v-model="spec.name" placeholder="如口味/尺寸" style="width: 100px;"
@change="onSpecNameChange" />
</el-form-item>
<el-form-item :label="'规格值'" style="flex:1;">
<el-input v-model="spec.valuesStr" type="textarea" :rows="2" placeholder="每行一个规格值"
@change="onSpecValueChange(specIdx)" />
</el-form-item>
<div class="flex justify-end">
<el-button type="danger" @click="removeSpec(specIdx)">删除</el-button>
</div>
</div>
<el-button type="primary" @click="addSpec" :disabled="specs.length >= 3"
class="mb-2">添加规格项</el-button>
<el-table :data="skuTable" style="width: 100%; margin-top: 10px;">
<el-table-column v-for="(spec, idx) in specs" :key="idx"
:label="spec.name || `规格${idx + 1}`" :prop="'spec' + idx" />
<el-table-column label="价格(元)">
<template #default="scope">
<el-input v-model="scope.row.price" placeholder="价格" />
</template>
</el-table-column>
<el-table-column label="成本价(元)">
<template #default="scope">
<el-input v-model="scope.row.cost_price" placeholder="成本价" />
</template>
</el-table-column>
<el-table-column label="库存">
<template #default="scope">
<el-input v-model="scope.row.stock" placeholder="库存" />
</template>
</el-table-column>
</el-table> -->
<sku v-model="formData"></sku>
</div>
</el-tab-pane>
<el-tab-pane label="商品详情">
@ -328,8 +189,8 @@ const formData = reactive({
second_category_id: '',
third_category_id: '',
remark: '',
image: '',
goods_image: '',
image: 'https://jianbing.stnav.com/uploads/images/20250523/20250523102712baf985773.jpg',
goods_image: ['https://jianbing.stnav.com/uploads/images/20250523/20250523102712baf985773.jpg'],
video: '',
poster: '',
one_price: '',
@ -341,7 +202,7 @@ const formData = reactive({
one_weight: '',
one_bar_code: '',
spec_name: '',
spec_type: 2,
spec_type: 1,
content: '',
sales_sum: 0,
click_count: 0,
@ -378,6 +239,33 @@ const formData = reactive({
is_member: 0,
give_integral: 0,
is_selffetch: '',
sku_name_list: [
{
name: "",
has_image: 0,
value: [
{
value: "",
image: ""
}
]
}
],
sku_list: [
{
id: "",
image: "",
sku_value_arr: [],
price: "",
line_price: "",
cost_price: "",
market_price: "",
stock: 0,
weight: 0,
volume: "",
code: ""
}
],
})
@ -405,174 +293,6 @@ const formRules = reactive<any>({
}],
})
/*******************************多规格开始**************************************/
import type { skuType, skuAttrItemType } from './components/type.d'
import { deepClone } from '@yipai-front-end/lib'
const skuAttributes: Ref<skuAttrItemType[]> = ref([])
const stockKeepUnits: Ref<skuType[]> = ref([])
let afterSku: skuType[] = []
const skuAttrItem: skuAttrItemType = {
title: '',
isAddImage: false,
values: [{ thumbnailUrl: '', attributeValue: '' }],
}
// 监听sku本身的变化,并将当前sku进行备份
watch(
() => stockKeepUnits.value,
(value) => {
console.log("value>>>", value);
afterSku = deepClone(value)
},
{ deep: true }
)
// 监听销售属性的变化,并构建sku
watch(
() => skuAttributes.value,
(value) => {
if (value.length) {
console.log("123>>>", 123);
generateSku(deepClone(value))
}
},
{ deep: true }
)
/**
* 更新销售属性构建sku
* @param skuAttribute
*/
function generateSku(skuAttribute: skuAttrItemType[]) {
let attrValue: any[] = []
skuAttribute.map((item) => {
attrValue.push(item.values)
})
let skus: any[] = []
if (attrValue.length === 0) {
stockKeepUnits.value = []
return
}
skus = attrValue.reduce((col: any[], set) => {
let res: any[] = []
col.forEach((c) => {
set.forEach((s) => {
let t = c.attributeValue + ',' + s.attributeValue
res.push({ attributeValue: t, thumbnailUrl: c.thumbnailUrl || s.thumbnailUrl || '' })
})
})
return res
})
// 增加,回显相关字段
skus.map((e: skuType) => {
// 寻找销售规格一致的副本数据
let old = afterSku.find((item) => item.attributeValue == e.attributeValue)
console.log('old=', old)
console.log('单项', e)
e.id = old == null ? '' : old.id
e.price = old == null ? '' : old.price
e.marketPrice = old == null ? '' : old.marketPrice
e.stock = old == null ? '' : old.stock
e.specificationBarCode = old == null ? '' : old.specificationBarCode
return e
})
console.log(skus, 'skus')
stockKeepUnits.value = skus
}
/**
* 删除销售属性
* @param index
*/
function deleteSkuAttr(index) {
skuAttributes.value.splice(index, 1)
}
/**
* 删除销售属性字段
* @param index
* @param cindex
*/
function deleteSkuAttrName(index: number, cindex: number) {
skuAttributes.value[index].values.splice(cindex, 1)
}
/**
* 增加sku属性图片
* @param index
* @param cindex
*/
async function addSkuAttrImage(index: number, cindex: number) {
let res = await chooseToFile()
console.log(res)
// 生产环境此处应该是上传到服务端,获取线上url
// 此处写法仅限测试,上传大图片可能造成卡顿
_blobToDataUrl(res[0], (res) => {
skuAttributes.value[index].values[cindex].thumbnailUrl = res
})
}
/**
* 增加销售属性字段
* @param index
*/
function addSkuAttrName(index: number) {
skuAttributes.value[index].values.push({ attributeValue: '', thumbnailUrl: '' })
}
/**
* 切换首个sku是否上传图片的状态
*/
function toggleSkuImg(index) {
let { isAddImage } = skuAttributes.value[index]
if (isAddImage) {
skuAttributes.value[index].values.map((e) => {
e.thumbnailUrl = ''
return e
})
}
skuAttributes.value[index].isAddImage = !isAddImage
}
/**
* 增加销售属性
*/
function addSkuAttr() {
skuAttributes.value.push(deepClone(skuAttrItem))
}
/**
* 动态更新表格字段
* @param index
* @param dataIndex
*/
function changeSkuData(index: number, dataIndex: string, evt: any) {
let value = evt.target.value
stockKeepUnits.value[index][dataIndex] = value
}
function save() {
// message.success('请查看控制台输出')
console.log('销售属性:', skuAttributes.value)
console.log('sku:', stockKeepUnits.value)
}
function _blobToDataUrl(file, callback) {
const reader = new FileReader()
reader.onload = () => {
const url = URL.createObjectURL(file) // 获取临时访问链接
callback(url)
}
reader.readAsDataURL(file)
}
/*******************************多规格结束**************************************/
// 获取详情
const setFormData = async (data: Record<any, any>) => {
@ -606,83 +326,45 @@ const getDetail = async (row: Record<string, any>) => {
formData['one_weight'] = item[0].weight
formData['one_bar_code'] = item[0].bar_code
} else if (base.spec_type == 2) {
console.log("456>>>", 456);
// 多规格
skuAttributes.value = [
{
isAddImage: false,
title: '口味',
values: [
{
attributeValue: '甜',
thumbnailUrl: ''
},
{
attributeValue: '辣',
thumbnailUrl: ''
}
]
},
{
isAddImage: false,
title: '尺寸',
values: [
{
attributeValue: '大',
thumbnailUrl: ''
},
]
}
]
const skuNameList = []
const skuList = []
spec.map(val => {
skuNameList.push({
has_image: 0,
name: val.name,
value: val.values,
spec_id: val.id
})
})
item.map((val, index) => {
skuList.push({
id: val.id,
ids: val.spec_value_ids,
sku_value_arr: val.spec_value_str.split(','),
value: val.spec_value_str.split(','),
cost_price: val.cost_price,
price: val.price,
stock: val.stock
})
})
// spec_id === spec.id
// spec_value_ids === item.spec_value_ids
// item_id === item.id
nextTick(() => {
formData.sku_name_list = skuNameList
formData.sku_list = skuList
});
stockKeepUnits.value = [
{
attributeValue: '甜,大',
id: '',
marketPrice: "2",
price: "1",
specificationBarCode: '',
stock: '3',
thumbnailUrl: ''
},
{
attributeValue: '辣,大',
id: '',
marketPrice: "2",
price: "1",
specificationBarCode: '',
stock: '3',
thumbnailUrl: ''
}
]
afterSku = [
{
attributeValue: '甜,大',
id: '',
marketPrice: "2",
price: "1",
specificationBarCode: '',
stock: '3',
thumbnailUrl: ''
},
{
attributeValue: '辣,大',
id: '',
marketPrice: "2",
price: "1",
specificationBarCode: '',
stock: '3',
thumbnailUrl: ''
}
]
// generateSku(deepClone(skuAttributes.value))
}
// 设置分类
selectFirstCategory(data.first_category_id)
selectSecondCategory(data.second_category_id)
}
//
let goodsCategory = reactive<any[]>([])
let firstCategory = reactive<any[]>([])
let secondCategory = reactive<any[]>([])
@ -726,23 +408,39 @@ const selectSecondCategory = (id: number) => {
// 提交按钮
const handleSubmit = async () => {
console.log('销售属性:', skuAttributes.value)
console.log('sku:', stockKeepUnits.value)
return false
// await formRef.value?.validate()
await formRef.value?.validate()
const data = { ...formData }
if (data.spec_type == 1 && !data.one_price && !data.one_cost_price && !data.one_stock) {
feedback.notifyError('请填写商品规格')
return false
}
console.log("attribute>>>", attribute);
data.spec_name = attribute.spec.map(item => item.name) // 规格名称
data.spec_values = attribute.spec.map(item => item.item.map(i => i.name).join(',')) // 规格项名称
data.spec_value_str = attribute.sku.map(item => item.sku) // 规格项笛卡尔积
data.price = attribute.sku.map(item => item.price)
data.cost_price = attribute.sku.map(item => item.cost_price)
data.stock = attribute.sku.map(item => item.stock)
console.log('data.sku_name_list:', data.sku_name_list)
console.log('data.sku_list:', data.sku_list)
data.spec_name = data.sku_name_list.map(item => item.name) // 规格名称:口味、尺寸
data.spec_values = data.sku_name_list.map(item => item.value.map(i => i.value).join(',')) // 规格项名称:"甜,辣""大"
data.spec_value_str = data.sku_list.map(item => item.sku_value_arr.join(',')) // 规格项笛卡尔积
data.price = data.sku_list.map(item => item.price)
data.cost_price = data.sku_list.map(item => item.cost_price)
data.stock = data.sku_list.map(item => item.stock)
data.spec_image = []
if (mode.value == 'edit') {
// spec_id === spec.id
// spec_value_ids === item.spec_value_ids
// item_id === item.id
data.spec_id = data.sku_name_list.map(item => item.spec_id) // 不用动
data.spec_value_ids = data.sku_name_list.map(item => item.value.map(i => i.id !== undefined ? i.id : 0).join(',')) // 每组的spec_id用逗号组合成字符串
data.item_id = data.sku_list.map(item => item.id) // 补空
}
delete data.sku_name_list
delete data.sku_list
// console.log('销售属性:', skuAttributes.value)
// console.log('sku:', stockKeepUnits.value)
console.log('data:', data)
// return false
if (data.spec_type == 2 && !data.price && !data.cost_price && !data.stock && !data.spec_value_str) {
feedback.notifyError('请完善商品多规格字段')
return false
@ -770,86 +468,6 @@ const handleClose = () => {
onMounted(() => {
getCategory()
// console.log('销售属性:', skuAttributes.value)
// console.log('sku:', stockKeepUnits.value)
// skuAttributes.value = [
// {
// isAddImage: false,
// title: '口味',
// values: [
// {
// attributeValue: '甜',
// thumbnailUrl: ''
// },
// {
// attributeValue: '辣',
// thumbnailUrl: ''
// }
// ]
// },
// {
// isAddImage: false,
// title: '尺寸',
// values: [
// {
// attributeValue: '大',
// thumbnailUrl: ''
// },
// ]
// }
// ]
// stockKeepUnits.value = [
// {
// attributeValue: '甜,大',
// id: '',
// marketPrice: "2",
// price: "1",
// specificationBarCode: '',
// stock: '3',
// thumbnailUrl: ''
// },
// {
// attributeValue: '辣,大',
// id: '',
// marketPrice: "2",
// price: "1",
// specificationBarCode: '',
// stock: '3',
// thumbnailUrl: ''
// }
// ]
// generateSku(deepClone(skuAttributes.value))
// skuAttributes = {
// 0: {
// 'isAddImage': false,
// 'title': '口味',
// 'values': {
// {
// 'attributeValue': '甜',
// 'thumbnailUrl': ''
// },
// {
// 'attributeValue': '辣',
// 'thumbnailUrl': ''
// }
// }
// },
// 1: {
// 'isAddImage': false,
// 'title': '尺寸',
// 'values': {
// {
// 'attributeValue': '大',
// 'thumbnailUrl': ''
// }
// }
// }
// }
});

View File

@ -1,178 +0,0 @@
<template>
<div class="goods-sku" v-if="goods">
<dl v-for="item in goods.specs" :key="item.id">
<dt>{{ item.name }}</dt>
<dd>
<template v-for="val in item.values" :key="val.name">
<img v-if="val.picture" :class="{ selected: val.selected, disabled: val.disabled }"
@click="changeSelectedStatus(item, val)" :src="val.picture" :title="val.name" />
<span v-else :class="{ selected: val.selected, disabled: val.disabled }"
@click="changeSelectedStatus(item, val)">{{ val.name }}</span>
</template>
</dd>
</dl>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
import powerSet from "./power-set.js";
const goods = ref();
let pathMap = {};
const getGoods = async () => {
//1135076无库存规格
//1369155859933827074
const res = await axios.get(
"http://pcapi-xiaotuxian-front-devtest.itheima.net/goods?id=1369155859933827074"
);
goods.value = res.data.result;
pathMap = getPathMap(goods.value);
initDosabledStatus(goods.value.specs, pathMap);
};
onMounted(() => {
getGoods();
});
//切换选中状态
const changeSelectedStatus = (item, val) => {
if (val.disabled) {
return;
}
//item同一排对象,val当前点击项
if (val.selected) {
val.selected = false;
} else {
item.values.forEach((val) => (val.selected = false));
val.selected = true;
}
updateDisabledStatus(goods.value.specs, pathMap);
//产出sku对象
const index = getSelectedValues(goods.value.specs).findIndex(
(item) => item === undefined
);
if (index > -1) {
} else {
const key = getSelectedValues(goods.value.specs).join("-");
const skuIds = pathMap[key];
const skuObj = goods.value.skus.find((item) => item.id === skuIds[0]);
console.log("skuObj", skuObj);
}
};
//生成有效路径字典对象
const getPathMap = (goods) => {
const pathMap = {};
// 1.根据库存字段得到有效的sku数组
const effectiveSkus = goods.skus.filter((sku) => sku.inventory > 0);
// 2.根据有效的sku数组使用powerSet算法得到所有子集
effectiveSkus.forEach((sku) => {
//2.1获取匹配的valueName组成的数组
const selectedValArr = sku.specs.map((val) => val.valueName);
//2.2使用算法获取子集
const valueNamePowerSet = powerSet(selectedValArr);
// 3.根据子集生成路径字典对象
valueNamePowerSet.forEach((arr) => {
//初始化key
const key = arr.join("-");
//如果已经存在当前key就往数组中直接添加skuId如果不存在直接做赋值
if (pathMap[key]) {
pathMap[key].push(sku.id);
} else {
pathMap[key] = [sku.id];
}
});
});
return pathMap;
};
//初始化禁用状态
const initDosabledStatus = (specs, pathMap) => {
specs.forEach((spe) => {
spe.values.forEach((val) => {
if (pathMap[val.name]) {
val.disabled = false;
} else {
val.disabled = true;
}
});
});
};
//获取相中项的数组
const getSelectedValues = (specs) => {
const arr = [];
specs.forEach((spe) => {
//找到values中selected为true的项然后把它的name字段添加到对应的位置
const selectedVal = spe.values.find((item) => item.selected);
arr.push(selectedVal ? selectedVal.name : undefined);
});
return arr;
};
//切换时更新禁用状态
const updateDisabledStatus = (specs, pathMap) => {
specs.forEach((spe, index) => {
const selectedValues = getSelectedValues(specs);
spe.values.forEach((val) => {
selectedValues[index] = val.name;
const key = selectedValues.filter((value) => value).join("-");
console.log("key", key);
if (pathMap[key]) {
val.disabled = false;
} else {
val.disabled = true;
}
});
});
};
</script>
<style scoped lang="scss">
@mixin sku-state-mixin {
border: 1px solid #e4e4e4;
margin-right: 10px;
cursor: pointer;
&.selected {
border-color: #27ba9b;
}
&.disabled {
opacity: 0.6;
border-style: dashed;
cursor: not-allowed;
}
}
.goods-sku {
padding-left: 10px;
padding-top: 20px;
dl {
display: flex;
padding-bottom: 20px;
align-items: center;
dt {
width: 50px;
color: #999;
}
dd {
flex: 1;
color: #666;
>img {
width: 50px;
height: 50px;
margin-bottom: 4px;
@include sku-state-mixin;
}
>span {
display: inline-block;
height: 30px;
line-height: 28px;
padding: 0 20px;
margin-bottom: 4px;
@include sku-state-mixin;
}
}
}
}
</style>

View File

@ -1,28 +0,0 @@
export default function bwPowerSet(originalSet) {
const subSets = []
// We will have 2^n possible combinations (where n is a length of original set).
// It is because for every element of original set we will decide whether to include
// it or not (2 options for each set element).
const numberOfCombinations = 2 ** originalSet.length
// Each number in binary representation in a range from 0 to 2^n does exactly what we need:
// it shows by its bits (0 or 1) whether to include related element from the set or not.
// For example, for the set {1, 2, 3} the binary number of 0b010 would mean that we need to
// include only "2" to the current set.
for (let combinationIndex = 0; combinationIndex < numberOfCombinations; combinationIndex += 1) {
const subSet = []
for (let setElementIndex = 0; setElementIndex < originalSet.length; setElementIndex += 1) {
// Decide whether we need to include current element into the subset or not.
if (combinationIndex & (1 << setElementIndex)) {
subSet.push(originalSet[setElementIndex])
}
}
// Add current subset to the list of all subsets.
subSets.push(subSet)
}
return subSets
}

View File

@ -1,240 +0,0 @@
<template>
<div>
<!-- 属性配置 -->
<el-button @click="addAttribute" type="success" size="mini">添加属性</el-button>
<el-table :data="attributes" border style="width: 100%">
<!-- <el-table-column prop="attributeName" label="属性名称">
<template v-slot="{ row }">
<el-input v-model="row.attributeValues" placeholder="用竖线(|)分隔,如:蓝色|白色|灰色"></el-input>
</template>
</el-table-column> -->
<el-table-column label="属性值">
<template v-slot="{ row }">
<el-input v-model="row.attributeValues" placeholder="用竖线(|)分隔,如:蓝色|白色|灰色"></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template v-slot="{ $index }">
<el-button size="mini" type="danger" @click="removeAttribute($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- SKU列表 -->
<el-button @click="handleMakeSku" size="mini" style="margin-top: 20px" type="warning">生成SKU</el-button>
<el-button @click="batchSetVisible = true" size="mini">批量设置</el-button>
<el-table :data="skus" border style="width: 100%;" @cell-change="handleSkuChange">
<el-table-column prop="spec_key" label="规格键名" header-align="center"></el-table-column>
<el-table-column prop="spec_name" label="规格名称" header-align="center"></el-table-column>
<el-table-column prop="store_count" label="库存" header-align="center">
<template v-slot="{ row }">
<el-input size="mini" v-model.number="row.stock" @input="handleSkuChange(row)"></el-input>
</template>
</el-table-column>
<el-table-column prop="price" label="销售价格" header-align="center">
<template v-slot="{ row }">
<el-input-number v-model="row.price" size="mini" controls-position="right" :precision="2"
:step="0.01" :min="0" @change="handleSkuChange(row)"></el-input-number>
</template>
</el-table-column>
<el-table-column prop="cost_price" label="成本价" header-align="center">
<template v-slot="{ row }">
<el-input-number v-model="row.cost_price" size="mini" controls-position="right" :precision="2"
:step="0.01" :min="0" @change="handleSkuChange(row)"></el-input-number>
</template>
</el-table-column>
<el-table-column label="操作" width="100" header-align="center">
<template v-slot="{ $index }">
<el-button size="mini" type="danger" @click="removeSku($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 批量设置 -->
<!-- <el-button @click="batchSetVisible = true">批量设置</el-button>-->
<!-- <el-dialog title="批量设置" :visible.sync="batchSetVisible">
<el-form :model="batchForm">
<el-form-item label="价格" :label-width="formLabelWidth">
<el-input v-model="batchForm.price"></el-input>
</el-form-item>
<el-form-item label="库存" :label-width="formLabelWidth">
<el-input v-model.number="batchForm.store_count"></el-input>
</el-form-item>
<el-form-item label="成本价" :label-width="formLabelWidth">
<el-input v-model="batchForm.cost_price"></el-input>
</el-form-item>
<el-form-item label="市场价" :label-width="formLabelWidth">
<el-input v-model="batchForm.market_price"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="batchSetVisible = false">取消</el-button>
<el-button type="primary" @click="applyBatchSet">确定</el-button>
</span>
</el-dialog> -->
</div>
</template>
<script>
export default {
props: {
initialSkus: {
type: Array,
default: () => []
}
},
data() {
return {
attributes: [], // 存储属性及其值
skus: [], // 存储生成的SKU列表
batchSetVisible: false, // 控制批量设置对话框显示状态
batchForm: {
price: '',
store_count: null,
cost_price: null,
market_price: null
},
formLabelWidth: '80px'
}
},
watch: {
skus: {
deep: true,
handler(newValue) {
this.$emit('sku-updated', newValue)
}
}
},
created() {
// 初始化时从props中获取已有SKU数据并解析出属性和SKU列表
if (this.initialSkus.length > 0) {
this.parseInitialSkus(this.initialSkus)
}
},
methods: {
addAttribute() {
this.attributes.push({ attributeName: '', attributeValues: '' })
},
removeAttribute(index) {
this.attributes.splice(index, 1)
},
handleMakeSku() {
console.log('handleMakeSku')
if (this.attributes.length === 0) {
this.$message.error('请先添加属性')
return
}
if (this.skus.length > 0) {
this.$confirm('已存在SKU是否覆盖', '操作提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.generateSkus()
}).catch(() => {
// 取消操作
})
} else {
this.generateSkus()
}
},
generateSkus() {
// 清空现有的SKU列表
this.skus = []
// 获取所有属性值并创建所有可能的组合
const combinations = this.createCombinations(this.attributes)
// 根据组合生成SKUs
combinations.forEach(combination => {
const specKey = combination.join('_')
const specName = combination.join(';')
this.skus.push({
spec_key: specKey,
spec_name: specName,
store_count: 0,
price: 0.00,
cost_price: 0.00,
market_price: 0.00
})
})
},
createCombinations(attributes) {
const valueLists = attributes.map(attr => attr.attributeValues.split('|').map(val => val.trim()))
const result = []
function combine(tempArray, index) {
if (index === valueLists.length) {
result.push(tempArray.slice())
return
}
for (let i = 0; i < valueLists[index].length; i++) {
tempArray[index] = valueLists[index][i]
combine(tempArray, index + 1)
}
}
combine([], 0)
return result
},
removeSku(index) {
this.skus.splice(index, 1)
},
applyBatchSet() {
const { price, store_count, cost_price, market_price } = this.batchForm
// 更新所有SKU对应的字段
this.skus.forEach(sku => {
if (price !== '') sku.price = parseFloat(price)
if (store_count !== null) sku.store_count = store_count
if (cost_price !== null) sku.cost_price = cost_price
if (market_price !== null) sku.market_price = market_price
})
// 关闭对话框
this.batchSetVisible = false
},
handleSkuChange(row) {
// 触发更新事件将最新的SKU数据传递给父组件
this.$emit('sku-updated', this.skus)
},
parseInitialSkus(initialSkus) {
// 创建一个映射来保存每个属性名称及其对应的值集合
const attributeMap = new Map()
// 遍历初始SKU数据提取所有属性和对应的值
initialSkus.forEach(sku => {
// 提取属性名称
const [color, size] = sku.spec_key.split('_')
// 提取属性值
const values = sku.spec_name.split(';')
// 添加属性值到集合中
values.forEach(value => {
if (!attributeMap.has(color)) {
attributeMap.set(color, [])
}
if (!attributeMap.has(size)) {
attributeMap.set(size, [])
}
attributeMap.get(color).push(value.trim())
attributeMap.get(size).push(value.trim())
})
})
// 设置初始SKU列表
this.skus = initialSkus
// 构建属性对象数组,并确保每个属性值是按规则分隔的字符串,fixme 这里有问题,不是和生成一致的
/* this.attributes = Array.from(attributeMap.entries()).map(([key, values]) => ({
attributeName: key,
// 将数组转换为字符串,使用' | '作为分隔符
attributeValues: values.sort().join(' | ')
})) */
}
}
}
</script>