Files
ss-crm-manage-web/src/views/MiniMall/Product/add.vue
2024-06-14 15:55:48 +08:00

531 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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
if (!id) {
await ProductApi.createProduct(data)
message.success(t('common.createSuccess'))
} else {
await ProductApi.updateProduct(data)
message.success(t('common.updateSuccess'))
}
}
} 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 () => {
if (route.query?.id) {
formLoading.value = true
try {
const res = await ProductApi.getProduct(route.query.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: 'https://ss-cloud.ahduima.com/1001/1796426117251600384.png',
carouselImages: ['https://ss-cloud.ahduima.com/1001/1796426117251600384.png'],
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>