Files
gxtyzx-admin/src/views/shop/goods/components/goodsEdit.vue
T
2024-07-27 13:53:11 +08:00

814 lines
24 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>
<ele-modal
:width="900"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑商品' : '添加商品'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 3, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 24, sm: 24, xs: 24 } : { flex: '1' }
"
>
<a-tabs type="card" v-model:active-key="active" @change="onChange">
<a-tab-pane tab="基本信息" key="base">
<a-form-item label="商品类型" name="type">
<a-space>
<a-button
:type="form.type == 1 ? 'primary' : ''"
@click="onType(1)"
:ghost="form.type == 1"
>实物商品</a-button
>
<a-button
:type="form.type == 2 ? 'primary' : ''"
@click="onType(2)"
:ghost="form.type == 2"
>虚拟商品</a-button
>
</a-space>
<div class="ele-text-placeholder">
{{
form.type == 1
? `支持快递邮寄、同城配送或到店自提方式发货`
: '电子券码等,线下到店核销,无需备货'
}}
</div>
</a-form-item>
<a-form-item
label="选择店铺"
name="merchantId"
>
<SelectMerchant
:placeholder="`选择商户`"
class="input-item"
style="width: 558px"
v-model:value="form.merchantName"
@done="chooseMerchantId"
/>
</a-form-item>
<a-form-item label="商品名称" name="goodsName">
<a-input
allow-clear
style="width: 558px"
placeholder="请输入商品名称"
v-model:value="form.goodsName"
/>
</a-form-item>
<a-form-item label="商品分类" name="categoryId">
<SelectGoodsCategory
:data="data"
placeholder="请选择商品分类"
style="width: 558px"
v-model:value="category"
@done="chooseGoodsCategory"
/>
</a-form-item>
<a-form-item label="商品卖点" name="comments">
<a-input
allow-clear
:maxlength="60"
style="width: 558px"
placeholder="此款商品美观大方 性价比较高 不容错过"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="单位名称" name="unitName">
<a-input
allow-clear
style="width: 558px"
placeholder="单位名称,如(个)"
v-model:value="form.unitName"
/>
</a-form-item>
<a-form-item label="商品价格" name="price">
<a-input-number
:placeholder="`商品价格`"
style="width: 240px"
v-model:value="form.price"
/>
<div class="ele-text-placeholder">商品的实际购买金额,最低0.01</div>
</a-form-item>
<a-form-item label="市场价" name="salePrice">
<a-input-number
:placeholder="`市场价`"
style="width: 240px"
v-model:value="form.salePrice"
/>
<div class="ele-text-placeholder">划线价仅用于商品页展示</div>
</a-form-item>
<a-form-item label="当前库存" name="stock">
<a-input-number
:placeholder="`商品库存`"
style="width: 240px"
v-model:value="form.stock"
/>
<div class="ele-text-placeholder">划线价仅用于商品页展示</div>
</a-form-item>
<a-form-item label="商品图片" name="image">
<SelectFile
:placeholder="`请选择视频文件`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
<div class="ele-text-placeholder"
>支持上传视频(mp4格式),视频时长不超过60秒,视频大小不超过200M。</div
>
</a-form-item>
<a-form-item label="轮播图" name="files">
<SelectFile
:placeholder="`请选择视频文件`"
:limit="9"
:data="files"
@done="chooseFile"
@del="onDeleteFile"
/>
</a-form-item>
<a-form-item label="状态" name="isShow">
<a-radio-group v-model:value="form.isShow">
<a-radio :value="1">上架</a-radio>
<a-radio :value="0">下架</a-radio>
</a-radio-group>
</a-form-item>
</a-tab-pane>
<a-tab-pane tab="商品规格" key="spec">
<a-form-item label="规格类型" name="specs">
<a-radio-group v-model:value="form.specs">
<a-radio :value="0">单规格</a-radio>
<a-radio :value="1">多规格</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="multipleSpec" v-if="form.specs == 1">
<div class="w-[300px] ml-10">
<SelectSpec
placeholder="选择规格"
:width="130"
v-model:value="form.specName"
@done="onSpec"
/>
</div>
</a-form-item>
<a-form-item name="specValue" v-if="form.specs == 1">
<a-space direction="vertical" class="ml-[40px]">
<template v-for="(item, index) in spec" :key="index">
<div class="text-left flex items-center leading-10 text-gray-400">
<div class="mr-2">{{ item.value }} :</div>
<CloseCircleOutlined
class="cursor-pointer"
@click="onClose(index)"
/>
</div>
<ele-edit-tag
v-model:data="item.detail"
size="middle"
shape="round"
/>
</template>
<a-card class="ml-[40px]" v-if="showSpecForm">
<a-form-item name="name">
<a-input
allow-clear
placeholder="请输入规格"
v-model:value="name"
/>
</a-form-item>
<a-form-item name="value">
<a-input
allow-clear
placeholder="请输入规格值"
v-model:value="value"
/>
</a-form-item>
<a-space>
<a-button type="primary" @click="addSpecValue">确定</a-button>
<a-button @click="openSpecForm">取消</a-button>
</a-space>
</a-card>
<a-space v-if="spec.length > 0">
<a-button type="primary" class="mt-5" @click="openSpecForm"
>添加新规格</a-button
>
<a-button type="primary" class="mt-5" @click="generateSku"
>生成SKU</a-button
>
</a-space>
</a-space>
</a-form-item>
<a-form-item name="oneSpec" v-if="form.specs == 1">
<div class="w-full">
<div class="sku-table">
<a-table
:pagination="false"
:dataSource="skuList"
:columns="columns"
:scroll="{ y: 500 }"
>
<template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'line'">
{{ index + 1 }}
</template>
<template v-if="column.key === 'image'">
<SelectFile
:placeholder="`请选择商品图片`"
:limit="1"
:data="record.images || []"
:index="index"
@done="chooseSkuImage"
@del="onDeleteSkuItem"
/>
</template>
<template v-if="column.key === 'price'">
<a-input :placeholder="`成本价`" v-model:value="skuList[index].price" />
</template>
<template v-if="column.key === 'salePrice'">
<a-input :placeholder="`售价`" v-model:value="record.salePrice" />
</template>
<template v-if="column.key === 'stock'">
<a-input :placeholder="`库存`" v-model:value="record.stock" />
</template>
<template v-if="column.key === 'skuNo'">
<a-input :placeholder="`编码`" v-model:value="record.skuNo" />
</template>
</template>
</a-table>
</div>
</div>
</a-form-item>
</a-tab-pane>
<a-tab-pane tab="商品详情" key="content">
<a-form-item name="content">
<!-- 编辑器 -->
<tinymce-editor
ref="editorRef"
class="content"
v-model:value="content"
:disabled="disabled"
:init="config"
placeholder="图片直接粘贴自动上传"
@paste="onPaste"
/>
</a-form-item>
</a-tab-pane>
<a-tab-pane tab="营销设置" key="coupon">
<a-form-item label="货架" name="position">
<a-input
allow-clear
style="width: 250px"
placeholder="请输入货架"
v-model:value="form.position"
/>
</a-form-item>
<a-form-item label="商品重量" name="goodsWeight">
<a-input
allow-clear
style="width: 250px"
placeholder="请输入商品重量"
v-model:value="form.goodsWeight"
/>
</a-form-item>
<a-form-item label="销量" name="sales">
<a-input-number
allow-clear
style="width: 250px"
placeholder="请输入销量"
v-model:value="form.sales"
/>
</a-form-item>
<a-form-item label="获取积分" name="gainIntegral">
<a-input-number
:placeholder="`消费获取的积分`"
style="width: 250px"
v-model:value="form.gainIntegral"
/>
</a-form-item>
<a-form-item label="库存计算方式" name="deductStockType">
<a-radio-group v-model:value="form.deductStockType">
<a-radio :value="20">付款减库存</a-radio>
<a-radio :value="10">下单减库存</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
style="width: 250px"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-tab-pane>
</a-tabs>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { CloseCircleOutlined } from '@ant-design/icons-vue';
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addGoods, updateGoods } from "@/api/shop/goods";
import { Goods } from '@/api/shop/goods/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance, RuleObject } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import { Merchant } from '@/api/shop/merchant/model';
import TinymceEditor from '@/components/TinymceEditor/index.vue';
import { uploadFile, uploadOss } from "@/api/system/file";
import { SpecValue } from "@/api/shop/specValue/model";
import { Spec } from "@/api/shop/spec/model";
import { GoodsSku } from "@/api/shop/goodsSku/model";
import { GoodsSpec } from "@/api/shop/goodsSpec/model";
import { generateGoodsSku, listGoodsSku } from "@/api/shop/goodsSku";
import { listGoodsSpec } from "@/api/shop/goodsSpec";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Goods | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const content = ref('');
const disabled = ref(false);
// 当前选项卡
const active = ref('base');
const spec = ref<SpecValue[]>([]);
const showSpecForm = ref(false);
const name = ref();
const value = ref();
const skuList = ref<GoodsSku[]>([]);
const files = ref<ItemType[]>([]);
const goodsSpec = ref<GoodsSpec>();
const category = ref<string[]>([]);
const columns = [
{
title: '图片',
dataIndex: 'image',
key: 'image',
align: 'center'
},
{
title: '售价',
dataIndex: 'salePrice',
key: 'salePrice',
align: 'center',
},
{
title: '成本价',
dataIndex: 'price',
key: 'price',
align: 'center',
},
{
title: '库存',
dataIndex: 'stock',
key: 'stock',
align: 'center',
},
{
title: '商品编号',
dataIndex: 'skuNo',
key: 'skuNo',
align: 'center',
},
];
// 用户信息
const form = reactive<Goods>({
goodsId: undefined,
type: 1,
code: undefined,
goodsName: undefined,
image: undefined,
content: undefined,
unitName: '',
categoryId: undefined,
categoryParent: undefined,
categoryChildren: undefined,
specs: 0,
position: undefined,
price: undefined,
salePrice: undefined,
files: undefined,
sales: 0,
gainIntegral: 0,
goodsWeight: undefined,
recommend: undefined,
merchantId: undefined,
merchantName: undefined,
stock: 1000,
deductStockType: 20,
isShow: 1,
status: 0,
comments: '',
sortNumber: 100,
specName: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
type: [
{
required: true,
message: '请选择商品类型',
type: 'number',
trigger: 'blur'
}
],
image: [
{
required: true,
message: '请上传图片',
type: 'string',
trigger: 'blur'
}
],
files: [
{
required: true,
message: '请上传轮播图',
type: 'string',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (form.files?.length == 0) {
return Promise.reject('选择上传轮播图');
}
return Promise.resolve();
}
}
],
specs: [
{
required: true,
message: '请选择规格类型',
type: 'number',
trigger: 'blur'
}
],
price: [
{
required: true,
message: '请填写商品价格',
type: 'number',
trigger: 'blur'
}
],
stock: [
{
required: true,
message: '请填写商品库存',
type: 'number',
trigger: 'blur'
}
],
merchantId: [
{
required: true,
message: '请选择店铺',
type: 'number',
trigger: 'blur'
}
],
categoryId: [
{
required: true,
type: 'string',
message: '选择商品分类',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (!form.categoryId) {
return Promise.reject('选择商品分类');
}
return Promise.resolve();
}
}
],
goodsName: [
{
required: true,
message: '请选择商品名称',
type: 'string',
trigger: 'blur'
}
],
sortNumber: [
{
required: true,
message: '请输入排序号',
type: 'number',
trigger: 'blur'
}
]
});
const onType = (index: number) => {
form.type = index;
};
/* 搜索 */
const chooseMerchantId = (item: Merchant) => {
form.merchantName = item.merchantName;
form.merchantId = item.merchantId;
};
const chooseGoodsCategory = (item,value) => {
form.categoryId = value[1].value;
form.categoryParent = value[0].label;
form.categoryChildren = value[1].label;
}
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const chooseSkuImage = (data: FileRecord) => {
const index = data?.index;
skuList.value[index].images?.push({
uid: uuid(),
url: data.path,
status: 'done'
});
skuList.value[index].image = data.path;
}
const onDeleteSkuItem = (index: number) => {
images.value.splice(index, 1);
};
const onChange = (text: string) => {
// 加载商品多规格
if(text == 'spec'){
const goodsId = props.data?.goodsId;
if(goodsId){
listGoodsSpec({goodsId}).then(data => {
console.log(data[0].specValue);
const specValue = data[0].specValue;
if(specValue){
spec.value = JSON.parse(specValue).map( d => {
console.log(d);
return {
value: d.value,
detail: d.detail
};
})
}
console.log(spec.value);
})
listGoodsSku({goodsId}).then(data => {
skuList.value = data;
})
}
}
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const onClose = (index) => {
spec.value.splice(index, 1);
};
const openSpecForm = () => {
showSpecForm.value = !showSpecForm.value;
};
const onSpec = (row: Spec) => {
form.specName = row.specName;
goodsSpec.value = row;
if(row.specValue){
spec.value = JSON.parse(row?.specValue);
}
}
// 新增规格
const addSpecValue = () => {
if (!name.value || !value.value) {
message.error(`请输入规格和规格值`);
return false;
}
const findIndex = spec.value.findIndex((d) => d.value == name.value);
if (findIndex == 0) {
message.error(`${name.value}已存在)`);
return false;
}
spec.value.push({
value: name.value,
detail: [value.value]
});
name.value = '';
value.value = '';
openSpecForm();
};
const chooseFile = (data: FileRecord) => {
files.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
};
const onDeleteFile = (index: number) => {
files.value.splice(index, 1);
};
/**
* 生成商品SKU列表
*/
const generateSku = () => {
generateGoodsSku(spec.value).then(data => {
if(data){
skuList.value = data;
}
})
}
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
const config = ref({
height: 240,
plugins: 'code preview fullscreen searchreplace save autosave link autolink image media table codesample lists advlist charmap emoticons anchor directionality pagebreak quickbars nonbreaking visualblocks visualchars wordcount',
toolbar: false,
images_upload_handler: (blobInfo, success, error) => {
const file = blobInfo.blob();
const formData = new FormData();
formData.append('file', file, file.name);
uploadOss(file).then(res => {
success(res.path)
}).catch((msg) => {
error(msg);
})
},
});
/* 粘贴图片上传服务器并插入编辑器 */
const onPaste = (e) => {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
let hasFile = false;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
let file = items[i].getAsFile();
const item: ItemType = {
file,
uid: (file as any).lastModified,
name: file.name
};
uploadFile(<File>item.file)
.then((result) => {
const addPath = `<p><img class="content-img" src="${result.url}"></p>`;
content.value = content.value + addPath
})
.catch((e) => {
message.error(e.message);
});
hasFile = true;
}
}
if (hasFile) {
e.preventDefault();
}
}
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
content: content.value,
files: JSON.stringify(files.value),
goodsSpec: goodsSpec.value,
goodsSkus: skuList.value
};
const saveOrUpdate = isUpdate.value ? updateGoods : addGoods;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
category.value = [];
files.value = [];
if (props.data) {
assignObject(form, props.data);
if (props.data.image) {
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
});
}
if(props.data.files){
files.value = JSON.parse(props.data.files);
}
if(props.data.goodsSpecs){
goodsSpec.value = props.data.goodsSpecs[0];
if(props.data.specs == 1){
form.specName = props.data.goodsSpecs[0].specName;
}
}
if(props.data.goodsSkus){
skuList.value = props.data.goodsSkus.map(d => {
d.images = [];
d.images.push({
uid: uuid(),
url: d.image,
status: 'done'
})
return d;
});
}
// 商品分类
if(props.data.categoryParent){
category.value.push(props.data.categoryParent);
}
if(props.data.categoryChildren){
category.value.push(props.data.categoryChildren);
}
if (props.data.content){
content.value = props.data.content;
}
isUpdate.value = true;
} else {
spec.value = [];
goodsSpec.value = undefined;
skuList.value = [];
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>