完善sku
This commit is contained in:
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
39
src/views/goods/components/type.d.ts
vendored
39
src/views/goods/components/type.d.ts
vendored
@ -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
|
||||
}[]
|
||||
}
|
||||
@ -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>
|
||||
@ -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': ''
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -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
|
||||
}
|
||||
@ -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>
|
||||
Reference in New Issue
Block a user