<template>
<div class="edit-popup">
<popup ref="popupRef" :title="popupTitle" :async="true" width="80%" @confirm="handleSubmit"
@close="handleClose">
<el-form ref="formRef" :model="formData" label-width="15%" :rules="formRules">
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" clearable placeholder="请输入标题" />
</el-form-item>
<el-form-item label="图片" prop="images">
<el-input v-model="formData.images" clearable placeholder="请输入图片" />
</el-form-item>
<el-form-item label="库存(展示使用)" prop="inventory">
<el-input v-model="formData.inventory" clearable placeholder="请输入库存(展示使用)" />
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input v-model="formData.price" clearable placeholder="请输入价格" />
</el-form-item>
<el-form-item label="详情" prop="details">
<el-card header="" shadow="never" class="!border-none">
<editor v-model="formData.details" height="500px" />
</el-card>
</el-form-item>
<el-form-item label="商品所属分类(逗号拼接)" prop="goods_classify_ids">
<el-input v-model="formData.goods_classify_ids" clearable placeholder="请输入商品所属分类(逗号拼接)" />
</el-form-item>
<el-form-item label="是否热门:0=否,1=是" prop="status_hot">
<el-input v-model="formData.status_hot" clearable placeholder="请输入是否热门:0=否,1=是" />
</el-form-item>
<el-form-item label="是否推荐:0=否,1=是" prop="status_recommend">
<el-input v-model="formData.status_recommend" clearable placeholder="请输入是否推荐:0=否,1=是" />
</el-form-item>
<el-form-item label="属性及其对应规格" prop="sku_data_log">
<el-input v-model="formData.sku_data_log" type="textarea" autosize clearable
placeholder="请输入是否推荐:0=否,1=是" />
</el-form-item>
<el-form-item label="属性及其对应规格" prop="sku_data_log">
<el-row><el-col><el-button type="primary" @click="addAttr()">新 增 属 性</el-button></el-col></el-row>
<el-row v-for="(attr_item, attr_index) in formData.sku_data_list" :key="attr_index"
class="attr-container">
<el-col :span="6">
<el-select v-model="attr_item.attr_id" placeholder="请选择属性"
:disabled="attr_item.attr_select">
<el-option v-for="(item1, index1) in attr_array_from_sql" :key="index1"
:label="item1.title" :value="item1.id">
</el-option>
</el-select>
</el-col>
<el-col :span="2"></el-col>
<el-col :span="16">
<!-- 绑定在attr_item.child.attr_spec_id里 -->
<el-select multiple v-model="attr_item.child_attr_spec_ids" placeholder="请选择规格"
@focus="handleFocus(attr_item.attr_id, attr_index)"
@change="(selectedIds) => handleChange(selectedIds, attr_item, attr_index)">
<!-- 这个选择器里的内容是靠属性id(attr_id)去数据库里筛选出来的属性规格列表 -->
<el-option v-for="(item2, index2) in attr_item.type_arr_temp" :key="index2"
:label="item2.name" :value="item2.id">
</el-option>
</el-select>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="sku_child_string" prop="sku_child_string">
<el-input v-model="formData.sku_child_string" type="textarea" autosize clearable
placeholder="sku_child_string" />
</el-form-item>
<el-form-item label="sku_child_string" prop="sku_child_string">
<el-row class="sku-container">
<el-col :span="8" class="sku—item-container">sku名称</el-col>
<el-col :span="4" class="sku—item-container">图片</el-col>
<el-col :span="4" class="sku—item-container">价格</el-col>
<el-col :span="4" class="sku—item-container">vip价格</el-col>
<el-col :span="4" class="sku—item-container">库存</el-col>
</el-row>
<el-row v-for="(item, index) in formData.sku_child" class="sku-container" :key="index">
<el-col :span="8" class="sku—item-container"><el-input disabled v-model="item.sku_name"
class="sku-input"></el-input></el-col>
<el-col :span="4" class="sku—item-container"><material-picker v-model="item.image" /></el-col>
<el-col :span="4" class="sku—item-container"><el-input clearable v-model="item.price"
class="sku-input"></el-input></el-col>
<el-col :span="4" class="sku—item-container"><el-input clearable v-model="item.price_vip"
class="sku-input"></el-input></el-col>
<el-col :span="4" class="sku—item-container"><el-input clearable v-model="item.inventory"
class="sku-input"></el-input></el-col>
</el-row>
</el-form-item>
</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 } from '@/api/goods'
import { timeFormat } from '@/utils/util'
import type { PropType } from 'vue'
import { apiGoodsAttrLists } from '@/api/goods_attr'
import { apiGoodsAttrSpecLists } from '@/api/goods_attr_spec'
import { forEach } from 'lodash'
import { tr } from 'element-plus/es/locales.mjs'
import { number } from 'echarts'
defineProps({
dictData: {
type: Object as PropType<Record<string, any[]>>,
default: () => ({})
}
})
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const formData_string = ref('')
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const mode = ref('add')
let attr_array_from_sql: { id: number; title: string }[] = []
const attr_array_type_from_sql: { id: number; goods_attr_id: number; name: string }[] = []
// 弹窗标题
const popupTitle = computed(() => {
return mode.value == 'edit' ? '编辑商品表' : '新增商品表'
})
// 表单数据
const formData = reactive({
id: '',
title: '',
images: '',
inventory: '',
price: '',
details: '',
goods_classify_ids: '',
status_hot: '',
status_recommend: '',
sku_data_log: '',
sku_child: [] as Array<{ sku_name: string, sku_ids: string, goods_id: number, id: number, sku_name_string: string, image: string, price: string, price_vip: string, inventory: number }>,
sku_child_string: '',
status: 1,
sku_data_list: [] as Array<{ attr_select: boolean, attr_id: number | undefined, attr_name: string, child: { attr_spec_id: number | null, attr_spec_name: string }[], child_attr_spec_ids: number[], type_arr_temp: { goods_attr_id: number, id: number, name: string }[] }>
})
// 表单验证
const formRules = reactive<any>({
title: [{
required: true,
message: '请输入标题',
trigger: ['blur']
}],
images: [{
required: true,
message: '请输入图片',
trigger: ['blur']
}]
})
// 规格下拉框开始下拉时
const handleFocus = async (attr_id: number | undefined, attr_index: number) => {
const res = await apiGoodsAttrSpecLists({ goods_attr_id: attr_id })
console.log('当前的attr_index', attr_index, '规格列表,靠attr_id', attr_id, '去筛选,返回的数据是', res)
formData.sku_data_list[attr_index].type_arr_temp = res.lists
}
// 规格下拉框选择完成时
const handleChange = (
selectedIds: number[], // 用户选中的规格 id 数组,比如 [1, 3]
attr_item: any, // 当前这一行的数据对象,比如 { attr_id: 5, child: [1, 3] }
attr_index: number // 当前是第几个 select(可选)
) => {
if (selectedIds.length > 0) {
formData.sku_data_list[attr_index].attr_select = true
} else {
formData.sku_data_list[attr_index].attr_select = false
}
console.log('选中的规格 ids:', selectedIds, '当前 attr_item:', attr_item, '当前索引:', attr_index)
}
// 监听 formData 对象的变化,自动转为 JSON 字符串并同步到 formData_string
watch(
() => formData, // 返回 reactive 对象本身
(newVal) => {
console.log('formData变量发生改变,改变后的结果为', newVal)
},
{ deep: true } // 深度监听,因为 formData 内部属性可能会变
)
// 监听 sku_child 的变化,自动同步到 sku_child_string(JSON 字符串格式)
watch(
() => formData.sku_child,
(newVal) => {
try {
// 将 sku_child 数组转为 JSON 字符串(无格式化,紧凑格式,适合存储)
formData.sku_child_string = JSON.stringify(newVal, null, 0)
console.log('sku_child 已变化,同步到 sku_child_string:', newVal)
} catch (err) {
console.error('sku_child 转为 JSON 失败:', err)
formData.sku_child_string = '' // 出错时清空,避免页面报错
}
},
{ deep: true } // 深度监听,因为 sku_child 很可能是一个数组或对象,内部可能变化
)
// // 监听 sku_data_list 变化,自动同步到 sku_data_log,并优化 child 数据结构
// watch(
// () => formData.sku_data_list,
// (newVal: any[]) => {
// try {
// // 遍历每一个属性对象 item
// const updatedList = newVal.map((item: any) => {
// const selectedSpecIds = item.child_attr_spec_ids || [] // 当前选中的规格ID数组,如 [1, 2]
// const typeArrTemp = item.type_arr_temp || [] // 所有规格选项,含 id 和 name
// // 构建一个 id -> name 的映射,方便快速查找
// const specIdToNameMap = typeArrTemp.reduce((map: Record<number, string>, spec: any) => {
// if (spec.id !== undefined && spec.name !== undefined) {
// map[spec.id] = spec.name
// }
// return map
// }, {})
// // 构造新的 child 数组:只保留选中的规格,且每个对象只包含 attr_spec_id 和 attr_spec_name
// const newChild = selectedSpecIds.map((specId: number) => {
// const name = specIdToNameMap[specId] // 从 type_arr_temp 中根据 id 找到对应的 name
// return {
// attr_spec_id: specId,
// attr_spec_name: name || '' // 如果找不到 name,默认空字符串
// }
// })
// // 返回一个新对象,只包含你需要的三个字段:attr_id、attr_name、child
// return {
// attr_id: item.attr_id,
// attr_name: item.attr_name,
// child: newChild,
// // 为了调试的时候放便查看字段详情,故开发这三个字段
// // child_attr_spec_ids: item.child_attr_spec_ids,
// // type_arr_temp: item.type_arr_temp,
// // attr_select: item.attr_select
// // 为了调试的时候放便查看字段详情,故开发这三个字段
// }
// })
// // 将处理后的结构转为 JSON(无格式化,紧凑格式)
// formData.sku_data_log = JSON.stringify(updatedList, null, 0)
// console.log('sku_data_list', formData.sku_data_list, ' 已优化并同步到 sku_data_log:', updatedList)
// // 通过选择的属性和规格去组合sku
// console.log('当前formData.sku_data_list值为',formData.sku_data_list)
// generateSkus()
// } catch (err) {
// console.error('处理 sku_data_list 数据时出错:', err)
// formData.sku_data_log = ''
// }
// },
// { deep: true } // 深度监听
// )
// 监听 sku_data_list 变化
watch(
() => formData.sku_data_list,
(newVal: any[]) => {
const updatedList = newVal.map((item: any) => {
const selectedSpecIds = item.child_attr_spec_ids || [] // 👈 用户选中的规格ID
const typeArrTemp = item.type_arr_temp || []
const specIdToNameMap = typeArrTemp.reduce((map: any, spec: any) => {
if (spec.id !== undefined && spec.name !== undefined) {
map[spec.id] = spec.name
}
return map
}, {})
// 👇 这里才是根据用户选择,构造的新 child(只包含选中的!)
const newChild = selectedSpecIds.map((specId: number) => {
const name = specIdToNameMap[specId]
return {
attr_spec_id: specId,
attr_spec_name: name || ''
}
})
console.log('构造的新 child为变量newChild',newChild)
return {
attr_id: item.attr_id,
attr_name: item.attr_name,
child: newChild, // 👈 只有选中的规格!
// 原始的 child 没有被修改!
}
})
formData.sku_data_log = JSON.stringify(updatedList, null, 0)
// console.log('sku_data_list',formData.sku_data_list)
console.log('sku_data_list(updatedList) 已优化并同步到 sku_data_log:!!!!!!!!', updatedList)
generateSkus(updatedList) // 👈 然后调用生成 SKU
},
{ deep: true }
)
// 获取详情并设置表单数据
const setFormData = async (data: Record<any, any>) => {
console.log('接收到的原始数据 data =', data)
// 优先处理特殊字段:sku_data_log(假设它可能是 JSON 字符串)
if (typeof data.sku_data_log === 'string') {
try {
// 尝试将字符串解析为对象数组
formData.sku_data_list = JSON.parse(data.sku_data_log)
console.log('解析后的 sku_data_list =', formData.sku_data_list)
} catch (e) {
console.error('解析 sku_data_log 失败,将设置为空数组', e)
formData.sku_data_list = [] // 解析失败时设为空数组
}
} else if (Array.isArray(data.sku_data_log)) {
// 如果后端直接返回的是数组(不是字符串),也直接使用
formData.sku_data_list = data.sku_data_log
console.log('直接使用数组格式的 sku_data_log =', formData.sku_data_list)
} else {
// 其它情况(null / undefined / 非数组 / 非字符串),设为空数组
formData.sku_data_list = []
}
// 再处理其他普通字段(推荐明确赋值,而不是遍历 formData 的 key)
formData.id = data.id ?? ''
formData.title = data.title ?? ''
formData.images = data.images ?? ''
formData.inventory = data.inventory ?? ''
formData.price = data.price ?? ''
formData.details = data.details ?? ''
formData.goods_classify_ids = data.goods_classify_ids ?? ''
formData.status_hot = data.status_hot ?? ''
formData.status_recommend = data.status_recommend ?? ''
formData.sku_child = data.sku_child ?? ''
formData.sku_child_string = JSON.stringify(data.sku_child, null, 0)
// 如果你不想用 sku_data_log 字段,可以不赋值,或者赋值原始字符串
formData.sku_data_log = data.sku_data_log ?? ''
let attr_spec_ids: number[] = []
formData.sku_data_list.forEach((item1: any, index: any) => {
attr_spec_ids = []
//自己改变数据结构,生成attr_spec_id数组,以供选择器使用
item1.child.forEach((item2: any, index: any) => {
console.log('item2.attr_spec_id', item2.attr_spec_id)
attr_spec_ids.push(item2.attr_spec_id)
})
item1.child_attr_spec_ids = attr_spec_ids
console.log('sku_data_list.item1', item1)
// 构造一个新的数组,每个元素只包含 id 和 name,来源于 item1.child 中的 attr_spec_id 和 attr_spec_name
item1.type_arr_temp = item1.child.map((childItem: any) => ({
id: childItem.attr_spec_id,
name: childItem.attr_spec_name
}))
// 当规格被选择时,属性下拉框不可选;当规格未被选择时,属性下拉框可选
if (item1.child.length > 0) {
item1.attr_select = true
console.log('item1.child', item1.child, '的长度为', item1.child.length, '设置完后item1变量为', item1)
} else {
item1.attr_select = false
console.log('item1.child', item1.child, 'item1.child的长度为', item1.child.length, '设置完后item1变量为', item1)
}
})
// 打印最终表单数据,用于调试
console.log('最终设置的表单数据 formData =', formData)
}
// 新增属性
const addAttr = () => {
// 新增一条空的属性数据对象
const newAttrItem = {
attr_select: false, // 初始时属性可选(没有选规格时不禁用)
attr_id: undefined, // 默认值,未选择属性
attr_name: '', // 可选字段,可以不设置
child: [], // 当前属性下还没有规格
child_attr_spec_ids: [], // 用户还未选择任何规格
type_arr_temp: [] // 规格下拉选项,初始为空
}
// 将新对象添加到 sku_data_list 数组中
formData.sku_data_list.push(newAttrItem)
console.log('已新增一条属性,当前 sku_data_list 长度为:', formData.sku_data_list.length)
}
const getDetail = async (row: Record<string, any>) => {
const data = await apiGoodsDetail({
id: row.id
})
setFormData(data)
}
// 提交按钮
const handleSubmit = async () => {
await formRef.value?.validate()
const data = { ...formData, }
mode.value == 'edit'
? await apiGoodsEdit(data)
: await apiGoodsAdd(data)
popupRef.value?.close()
emit('success')
}
//打开弹窗
const open = async (type = 'add') => {
const res = await apiGoodsAttrLists()
console.log('属性数组', res.lists)
attr_array_from_sql = res.lists
console.log('属性数组', attr_array_from_sql)
mode.value = type
popupRef.value?.open()
}
// 关闭回调
const handleClose = () => {
emit('close')
}
defineExpose({
open,
setFormData,
getDetail
})
// 工具函数:计算多个数组的笛卡尔积(所有可能的组合)
const cartesianProduct = (arrays: any[][]): any[][] => {
if (arrays.length === 0) return [[]]
if (arrays.length === 1) return arrays[0].map(item => [item])
const [first, ...rest] = arrays
const restProduct = cartesianProduct(rest)
return first.flatMap(item =>
restProduct.map(restItem => [item, ...restItem])
)
}
// const generateSkus = (updatedList) => {
// console.log('生成sku使用的sku_data_listshu!!!!!!!!!!',formData.sku_data_list)
// // 1. 提取每个属性的 child(即规格列表),并过滤掉未选择规格的属性(child 为空的)
// const validSpecGroups = formData.sku_data_list
// .map(item => item.child)
// .filter(childList => childList && childList.length > 0) // 只保留有选中的规格的属性
// // 2. 检查是否还有任何属性被选中了规格
// if (validSpecGroups.length === 0) {
// console.warn('没有属性被选择任何规格,无法生成 SKU')
// formData.sku_child = [] // 不生成任何 SKU
// return
// }
// // 3. 计算所有 **被选中属性的规格** 的笛卡尔积(组合)
// const combinations = cartesianProduct(validSpecGroups)
// // 4. 遍历每一种组合,构造 SKU 对象
// const skuList = combinations.map((combination, index) => {
// // combination 是一个数组,如:
// // [
// // { attr_spec_id: 1, attr_spec_name: '红色' },
// // { attr_spec_id: 6, attr_spec_name: '8G' },
// // { attr_spec_id: 12, attr_spec_name: '大' }
// // ]
// // 构造 sku_ids:每个 attr_spec_id 用逗号拼接,如 "1,6,12"
// const sku_ids = combination.map(spec => spec.attr_spec_id).join(',')
// // 构造 sku_name:每个规格名称用 " - " 拼接,如 "红色 - 8G - 大"
// const sku_name = combination.map(spec => spec.attr_spec_name).join(' - ')
// // 构造 sku_name_string(可加商品前缀,比如 "测试1 " + sku_name)
// const sku_name_string = `测试1 ${sku_name}`
// // 构造一个标准的 SKU 对象
// const skuItem = {
// id: Date.now() + index, // 示例唯一ID,可以用实际商品SKU ID
// goods_id: 7, // 示例商品ID,请替换为真实值
// sku_ids: sku_ids,
// sku_name: sku_name,
// sku_name_string: sku_name_string,
// image: 'http://default.image.png', // 默认图片,可动态设置
// price: '100.00', // 默认价格
// price_vip: '0.00', // 默认VIP价格
// inventory: 1000, // 默认库存
// create_time: new Date().toISOString().slice(0, 19).replace('T', ' '),
// update_time: new Date().toISOString().slice(0, 19).replace('T', ' '),
// delete_time: null
// }
// return skuItem
// })
// // 5. 将生成的 SKU 列表赋值给 formData.sku_child
// formData.sku_child = skuList
// // 6. (可选)同步更新 sku_child_string 为 JSON 格式字符串
// formData.sku_child_string = JSON.stringify(skuList, null, 0)
// console.log('生成的SKU列表:', skuList)
// }
const generateSkus = (updatedList: any[]) => {
console.log('🎯 generateSkus 使用的是 updatedList(用户选中的规格):', updatedList)
// 1. 提取每个属性的 child(已为用户选中的规格),不再使用 formData.sku_data_list
const validSpecGroups = updatedList
.map(item => item.child) // 每个 item 是 { attr_id, attr_name, child: [...] }
.filter(childList => childList && childList.length > 0) // 只保留有选中规格的属性
// 2. 如果没有属性被选中任何规格,直接返回,不生成 SKU
if (validSpecGroups.length === 0) {
console.warn('⚠️ 没有属性被选择任何规格,无法生成 SKU')
formData.sku_child = [] // 不生成任何 SKU
return
}
// 3. 计算所有被选中属性规格的笛卡尔积(组合)
const combinations = cartesianProduct(validSpecGroups)
// 4. 遍历每一种组合,构造 SKU 对象
const skuList = combinations.map((combination, index) => {
// combination 是一个数组,如:
// [
// { attr_spec_id: 1, attr_spec_name: '红色' },
// { attr_spec_id: 10, attr_spec_name: 'S' }
// ]
// 构造 sku_ids:用逗号拼接所有选中的规格ID,如 "1,10"
const sku_ids = combination.map(spec => spec.attr_spec_id).join(',')
// 构造 sku_name:用 " - " 拼接所有选中的规格名称,如 "红色 - S"
const sku_name = combination.map(spec => spec.attr_spec_name).join(' - ')
// 构造 sku_name_string(可加商品前缀,如 "测试商品:" + sku_name)
const sku_name_string = `测试商品:${sku_name}`
// 5. 构造一个标准的 SKU 对象
const skuItem = {
id: Date.now() + index, // 示例唯一ID
goods_id: 7, // TODO: 替换为真实商品ID
sku_ids: sku_ids,
sku_name: sku_name,
sku_name_string: sku_name_string,
image: 'http://default.image.png', // 默认图片,可后续动态设置
price: '100.00', // 可改为从表单或规格中获取
price_vip: '0.00',
inventory: 1000, // 默认库存,可调整
create_time: new Date().toISOString().slice(0, 19).replace('T', ' '),
update_time: new Date().toISOString().slice(0, 19).replace('T', ' '),
delete_time: null
}
return skuItem
})
// 6. 将生成的 SKU 列表赋值给 formData.sku_child,用于页面展示
formData.sku_child = skuList
// 7. (可选)同步更新 sku_child_string 为 JSON 字符串
formData.sku_child_string = JSON.stringify(skuList, null, 0)
console.log('✅ 最终生成的SKU列表:', skuList)
}
</script>
<style>
.sku-input {
height: 32px;
}
.attr-container {
width: 100%;
height: 20px;
margin: 10px 0;
}
.sku-container {
width: 100%;
}
.sku—item-container {
display: flex;
justify-content: center;
padding: 0 10px;
align-items: center;
}
</style>