You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
533 lines
17 KiB
533 lines
17 KiB
<template>
|
|
<el-tabs v-model="tabName" type="border-card" tab-position="top" v-loading="formLoading">
|
|
<el-tab-pane label="商品信息" name="basic">
|
|
<el-form :model="form" ref="spuForm" :rules="rules" label-width="90px">
|
|
<el-row :gutter="20">
|
|
<el-col :span="8" :offset="0">
|
|
<el-form-item label="产品名称" prop="productName">
|
|
<el-input v-model="form.productName" placeholder="请输入产品名称" />
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="8" :offset="0">
|
|
<el-form-item label="分类" prop="productCategory">
|
|
<el-cascader
|
|
:options="opts.category"
|
|
v-model="form.productCategory"
|
|
placeholder="请选择分类"
|
|
:props="{ label: 'name', value: 'id' }"
|
|
filterable
|
|
show-all-levels
|
|
style="width: 100%"
|
|
/>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="8" :offset="0">
|
|
<el-form-item label="品牌" prop="productBrand">
|
|
<el-select v-model="form.productBrand" placeholder="请选择品牌" filterable>
|
|
<el-option
|
|
v-for="item in opts.brand"
|
|
:key="item.brandId"
|
|
:label="item.name"
|
|
:value="item.brandId"
|
|
/>
|
|
</el-select>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col
|
|
:span="8"
|
|
:offset="0"
|
|
v-for="fieldItem in diyFieldList"
|
|
:key="fieldItem.clueParamId"
|
|
>
|
|
<el-form-item :label="fieldItem.label" :prop="fieldItem.field">
|
|
<component :is="componentMap[fieldItem.component]" v-model="form[fieldItem.field]">
|
|
<template v-if="fieldItem.component == 'Select'">
|
|
<el-option
|
|
v-for="item in fieldItem.options"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</template>
|
|
<template v-else-if="fieldItem.component == 'Radio'">
|
|
<el-radio v-for="item in fieldItem.options" :key="item.id" :label="item.id">
|
|
{{ item.name }}
|
|
</el-radio>
|
|
</template>
|
|
<template v-else-if="fieldItem.component == 'Checkbox'">
|
|
<el-checkbox
|
|
v-for="item in fieldItem.options"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</template>
|
|
</component>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
<el-row :gutter="20">
|
|
<el-col :span="12" :offset="0">
|
|
<el-form-item label="产品简介" prop="productIntro">
|
|
<el-input
|
|
v-model="form.productIntro"
|
|
type="textarea"
|
|
:autosize="{ minRows: 4 }"
|
|
placeholder="请输入产品简介"
|
|
/>
|
|
</el-form-item>
|
|
</el-col>
|
|
<el-col :span="12" :offset="0">
|
|
<el-form-item label="主图" prop="mainImage">
|
|
<UploadImg v-model="form.mainImage" height="100px" width="100px" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
<el-row :gutter="20" v-if="!formLoading">
|
|
<el-col :span="24" :offset="0">
|
|
<el-form-item label="轮播图" prop="carouselImages">
|
|
<UploadImgs v-model="form.carouselImages" height="100px" width="100px" />
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
<el-row :gutter="20">
|
|
<el-col :span="24" :offset="0">
|
|
<el-form-item label="商品规格">
|
|
<el-button @click="handleAddSpec">添加规格</el-button>
|
|
<el-col v-for="(item, index) in form.productSpecList" :key="index">
|
|
<div>
|
|
<el-text class="mx-1">属性名:</el-text>
|
|
<el-tag class="mx-1" closable type="success" @close="handleCloseProperty(index)"
|
|
>{{ item.name }}
|
|
</el-tag>
|
|
</div>
|
|
<div>
|
|
<el-text class="mx-1">属性值:</el-text>
|
|
<el-tag
|
|
v-for="(value, valueIndex) in item.values"
|
|
:key="value.id"
|
|
class="mx-1"
|
|
closable
|
|
@close="handleCloseValue(index, valueIndex)"
|
|
>
|
|
{{ value.name }}
|
|
</el-tag>
|
|
<el-input
|
|
v-show="inputVisible(index)"
|
|
:id="`input${index}`"
|
|
:ref="setInputRef"
|
|
v-model="inputValue"
|
|
class="!w-20"
|
|
size="small"
|
|
@blur="handleInputConfirm(index, item.id)"
|
|
@keyup.enter="handleInputConfirm(index, item.id)"
|
|
/>
|
|
<el-button
|
|
v-show="!inputVisible(index)"
|
|
class="button-new-tag ml-1"
|
|
size="small"
|
|
@click="showInput(index)"
|
|
>
|
|
+ 添加
|
|
</el-button>
|
|
</div>
|
|
<el-divider class="my-10px" />
|
|
</el-col>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-table :data="form.skuList">
|
|
<el-table-column type="index" width="50" />
|
|
<el-table-column prop="specsName" label="规格名称">
|
|
<template #default="{ row }">
|
|
<el-input v-model="row.specsName" placeholder="请输入" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column
|
|
v-for="(col, index) in form.productSpecList"
|
|
:key="col.id"
|
|
:label="col.name"
|
|
>
|
|
<template #default="{ row }">
|
|
<span style="font-weight: bold; color: #40aaff">
|
|
{{ row.properties[index]?.valueName }}
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="price" label="销售价">
|
|
<template #default="{ row }">
|
|
<el-input-number v-model="row.price" :min="0.01" :step="1" />
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="intro" label="简介">
|
|
<template #default="{ row }">
|
|
<el-input v-model="row.intro" placeholder="请输入简介" />
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-form-item>
|
|
</el-col>
|
|
</el-row>
|
|
</el-form>
|
|
</el-tab-pane>
|
|
<el-tab-pane label="详细信息" name="detail">
|
|
<Editor v-model:modelValue="form.detailInfo" />
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
<div class="mt-20px flex justify-center">
|
|
<el-button type="primary" @click="onSubmit">保存</el-button>
|
|
<el-button plain @click="router.replace('/MiniMall/product')">返回列表</el-button>
|
|
</div>
|
|
<ProductAttributesAddForm ref="attributesAddFormRef" :propertyList="form.productSpecList" />
|
|
</template>
|
|
|
|
<script setup>
|
|
import { cloneDeep } from 'lodash-es'
|
|
import { getDiyFieldList } from '@/api/mall/product/productField'
|
|
import * as PropertyApi from '@/api/mall/product/property'
|
|
import * as ProductApi from '@/api/mall/product/index'
|
|
import ProductAttributesAddForm from './Comp/ProductAttributesAddForm.vue'
|
|
import * as BrandApi from '@/api/mall/product/brand'
|
|
import * as CategoryApi from '@/api/mall/product/category'
|
|
import { handleTree } from '@/utils/tree'
|
|
import { isObject } from '@/utils/is.ts'
|
|
import { componentMap } from '@/components/Form/src/componentMap'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const message = useMessage() // 消息弹窗
|
|
const { t } = useI18n() // 国际化
|
|
|
|
const tabName = ref('basic')
|
|
const form = ref({
|
|
productName: undefined,
|
|
productCategory: undefined,
|
|
productBrand: undefined,
|
|
productIntro: undefined,
|
|
mainImage: '',
|
|
carouselImages: [],
|
|
productSpecList: [],
|
|
skuList: [],
|
|
detailInfo: null,
|
|
status: 0
|
|
})
|
|
const rules = {
|
|
productName: { required: true, message: '产品名称不可为空', trigger: 'blur' }
|
|
}
|
|
const attributesAddFormRef = ref() // 添加商品属性表单
|
|
|
|
const opts = ref({
|
|
brand: [],
|
|
category: []
|
|
})
|
|
|
|
const diyFieldList = ref([])
|
|
async function getOptions() {
|
|
getDiyFieldList().then((data) => {
|
|
diyFieldList.value = data
|
|
})
|
|
BrandApi.getSimpleBrandList().then((data) => {
|
|
opts.value.brand = data || []
|
|
})
|
|
CategoryApi.getCategorySimpleList().then((data) => {
|
|
opts.value.category = handleTree(data || [])
|
|
})
|
|
}
|
|
|
|
/** 删除属性*/
|
|
function handleCloseProperty(index) {
|
|
form.value.productSpecList?.splice(index, 1)
|
|
getTableList()
|
|
}
|
|
|
|
const attributeIndex = ref(null)
|
|
// 输入框显隐控制
|
|
const inputVisible = computed(() => (index) => {
|
|
if (attributeIndex.value === null) return false
|
|
if (attributeIndex.value === index) return true
|
|
})
|
|
|
|
const inputValue = ref('') // 输入框值
|
|
/** 输入框失去焦点或点击回车时触发 */
|
|
async function handleInputConfirm(index, propertyId) {
|
|
if (inputValue.value) {
|
|
// 保存属性值
|
|
try {
|
|
const id = await PropertyApi.createPropertyValue({ propertyId, name: inputValue.value })
|
|
form.value.productSpecList[index].values.push({ id, name: inputValue.value })
|
|
message.success('添加成功')
|
|
} catch {
|
|
message.error('添加失败,请重试')
|
|
}
|
|
}
|
|
attributeIndex.value = null
|
|
inputValue.value = ''
|
|
getTableList()
|
|
}
|
|
|
|
/** 删除属性值*/
|
|
function handleCloseValue(index, valueIndex) {
|
|
form.value.productSpecList[index].values?.splice(valueIndex, 1)
|
|
getTableList()
|
|
}
|
|
|
|
function getTableList() {
|
|
const propertyList = [...form.value.productSpecList]
|
|
// 构建数据结构
|
|
const propertyValues = propertyList.map((item) =>
|
|
item.values.map((v) => ({
|
|
propertyId: item.id,
|
|
propertyName: item.name,
|
|
valueId: v.id,
|
|
valueName: v.name
|
|
}))
|
|
)
|
|
const buildSkuList = build(propertyValues)
|
|
// 如果回显的 sku 属性和添加的属性不一致则重置 skuList 列表
|
|
if (!validateData(propertyList)) {
|
|
// 如果不一致则重置表数据,默认添加新的属性重新生成 sku 列表
|
|
// form.value.skuList = []
|
|
}
|
|
form.value.skuList = []
|
|
for (const item of buildSkuList) {
|
|
const row = {
|
|
properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个 property 对象
|
|
price: 0.01,
|
|
intro: '',
|
|
specsName: ''
|
|
}
|
|
// 如果存在属性相同的 sku 则不做处理
|
|
const index = form.value.skuList.findIndex(
|
|
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
|
|
)
|
|
if (index !== -1) {
|
|
continue
|
|
}
|
|
form.value.skuList.push(row)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 生成 skuList 前置校验
|
|
*/
|
|
const validateData = (propertyList) => {
|
|
const skuPropertyIds = []
|
|
form.value.skuList.forEach((sku) =>
|
|
sku.properties.map((property) => {
|
|
if (skuPropertyIds.indexOf(property.propertyId) === -1) {
|
|
skuPropertyIds.push(property.propertyId)
|
|
}
|
|
})
|
|
)
|
|
const propertyIds = propertyList.map((item) => item.id)
|
|
return skuPropertyIds.length === propertyIds.length
|
|
}
|
|
|
|
/** 构建所有排列组合 */
|
|
const build = (propertyValuesList) => {
|
|
if (propertyValuesList.length === 0) {
|
|
return []
|
|
} else if (propertyValuesList.length === 1) {
|
|
return propertyValuesList[0]
|
|
} else {
|
|
const result = []
|
|
const rest = build(propertyValuesList.slice(1))
|
|
for (let i = 0; i < propertyValuesList[0].length; i++) {
|
|
for (let j = 0; j < rest.length; j++) {
|
|
// 第一次不是数组结构,后面的都是数组结构
|
|
if (Array.isArray(rest[j])) {
|
|
result.push([propertyValuesList[0][i], ...rest[j]])
|
|
} else {
|
|
result.push([propertyValuesList[0][i], rest[j]])
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
const inputRef = ref([]) //标签输入框Ref
|
|
/** 显示输入框并获取焦点 */
|
|
const showInput = async (index) => {
|
|
attributeIndex.value = index
|
|
inputRef.value[index].focus()
|
|
}
|
|
|
|
/** 解决 ref 在 v-for 中的获取问题*/
|
|
const setInputRef = (el) => {
|
|
if (el === null || typeof el === 'undefined') return
|
|
// 如果不存在id相同的元素才添加
|
|
if (!inputRef.value.some((item) => item.input?.attributes.id === el.input?.attributes.id)) {
|
|
inputRef.value.push(el)
|
|
}
|
|
}
|
|
|
|
function handleAddSpec() {
|
|
attributesAddFormRef.value.open()
|
|
}
|
|
|
|
const spuForm = ref()
|
|
|
|
function onSubmit() {
|
|
spuForm.value.validate(async (valid) => {
|
|
try {
|
|
if (valid && validateSku()) {
|
|
// 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
|
|
const deepCopyFormData = cloneDeep(unref(form.value))
|
|
deepCopyFormData.productSpecList = deepCopyFormData.skuList
|
|
if (deepCopyFormData.productCategory && deepCopyFormData.productCategory.length) {
|
|
deepCopyFormData.productCategory = deepCopyFormData.productCategory.at(-1)
|
|
}
|
|
delete deepCopyFormData.skuList
|
|
// 校验都通过后提交表单
|
|
let data = {
|
|
...deepCopyFormData,
|
|
diyParams: {}
|
|
}
|
|
for (let i = 0; i < diyFieldList.value.length; i++) {
|
|
const element = diyFieldList.value[i]
|
|
data.diyParams[element.field] = data[element.field]
|
|
}
|
|
const id = route.query.id || form.value.productId
|
|
if (!id) {
|
|
const resp = await ProductApi.createProduct(data)
|
|
message.success(t('common.createSuccess'))
|
|
form.value.productId = resp
|
|
} else {
|
|
await ProductApi.updateProduct(data)
|
|
message.success(t('common.updateSuccess'))
|
|
}
|
|
getDetail()
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 作为活动组件的校验
|
|
const ruleConfig = [
|
|
{
|
|
name: 'specsName',
|
|
rule: (arg) => arg.length,
|
|
message: '规格名称不可为空 !!!'
|
|
},
|
|
{
|
|
name: 'price',
|
|
rule: (arg) => arg >= 0.01,
|
|
message: '商品销售价格必须大于等于 0.01 元!!!'
|
|
}
|
|
]
|
|
|
|
/**
|
|
* 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
|
|
*/
|
|
const validateSku = () => {
|
|
let warningInfo = '请检查商品各行相关属性配置,'
|
|
let validate = form.value.skuList.length > 0 // 默认通过
|
|
if (!form.value.skuList.length) {
|
|
message.info('请添加商品规格!!!')
|
|
}
|
|
for (const sku of form.value.skuList) {
|
|
for (const rule of ruleConfig) {
|
|
const arg = getValue(sku, rule.name)
|
|
if (!rule.rule(arg)) {
|
|
validate = false // 只要有一个不通过则直接不通过
|
|
warningInfo += rule.message
|
|
break
|
|
}
|
|
}
|
|
// 只要有一个不通过则结束后续的校验
|
|
if (!validate) {
|
|
message.warning(warningInfo)
|
|
throw new Error(warningInfo)
|
|
}
|
|
}
|
|
return validate
|
|
}
|
|
|
|
const getValue = (obj, arg) => {
|
|
const keys = arg.split('.')
|
|
let value = obj
|
|
for (const key of keys) {
|
|
if (value && typeof value === 'object' && key in value) {
|
|
value = value[key]
|
|
} else {
|
|
value = undefined
|
|
break
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
|
|
const formLoading = ref(false)
|
|
|
|
/** 获得详情 */
|
|
const getDetail = async () => {
|
|
const id = route.query?.id || form.value.productId
|
|
if (id) {
|
|
formLoading.value = true
|
|
try {
|
|
const res = await ProductApi.getProduct(id)
|
|
let diyField = {}
|
|
if (res.diyParams) {
|
|
diyField = isObject(res.diyParams) ? res.diyParams : JSON.parse(res.diyParams)
|
|
}
|
|
const propList = getPropertyList(res?.productSpecList || [])
|
|
form.value = {
|
|
...res,
|
|
...diyField,
|
|
skuList: res?.productSpecList || [],
|
|
productSpecList: propList
|
|
}
|
|
} finally {
|
|
formLoading.value = false
|
|
}
|
|
} else {
|
|
form.value = {
|
|
productName: undefined,
|
|
productCategory: undefined,
|
|
productBrand: undefined,
|
|
productIntro: undefined,
|
|
mainImage: '',
|
|
carouselImages: [],
|
|
productSpecList: [],
|
|
skuList: [],
|
|
detailInfo: null,
|
|
status: 0
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获得商品的规格列表 - 商品相关的公共函数
|
|
*
|
|
* @param spu
|
|
* @return PropertyAndValues 规格列表
|
|
*/
|
|
const getPropertyList = (list) => {
|
|
// 直接拿返回的 skus 属性逆向生成出 propertyList
|
|
const properties = []
|
|
// 只有是多规格才处理
|
|
list.forEach((sku) => {
|
|
sku.properties?.forEach(({ propertyId, propertyName, valueId, valueName }) => {
|
|
// 添加属性
|
|
if (!properties?.some((item) => item.id === propertyId)) {
|
|
properties.push({ id: propertyId, name: propertyName, values: [] })
|
|
}
|
|
// 添加属性值
|
|
const index = properties?.findIndex((item) => item.id === propertyId)
|
|
if (!properties[index].values?.some((value) => value.id === valueId)) {
|
|
properties[index].values?.push({ id: valueId, name: valueName })
|
|
}
|
|
})
|
|
})
|
|
return properties
|
|
}
|
|
|
|
onMounted(async () => {
|
|
getOptions()
|
|
await getDetail()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped></style>
|
|
|