Files
guofu-admin/src/views/shop/goods/components/goodsEdit.vue
2024-09-27 14:17:12 +08:00

925 lines
28 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
v-if="!merchantId"
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" v-if="!merchantId">
<SelectGoodsCategory
:data="data"
placeholder="请选择商品分类"
style="width: 558px"
v-model:value="category"
@done="chooseGoodsCategory"
/>
</a-form-item>
<a-form-item label="菜品分类" name="categoryId" v-else>
<SelectGoodsCategory
:merchantId="merchantId"
placeholder="请选择商品分类"
style="width: 558px"
v-model:value="form.categoryId"
@done="chooseTakeawayCategory"
/>
</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="salePrice">
<a-input-number
:placeholder="`市场价`"
style="width: 240px"
:min="0.01"
v-model:value="form.salePrice"
/>
<div class="ele-text-placeholder">市场价或划线价仅用于商品页展示</div>
</a-form-item>
<a-form-item label="会员价" name="price">
<a-space>
<a-input-number
:placeholder="`会员价`"
style="width: 240px"
:min="0.01"
v-model:value="form.price"
/>
<a-checkbox v-model:checked="form.priceGift">有赠品</a-checkbox>
</a-space>
<div class="ele-text-placeholder">商品的实际购买金额最低0.01</div>
</a-form-item>
<a-form-item label="经销商" name="buyingPrice">
<a-space>
<a-input-number
:placeholder="`经销商价格`"
style="width: 240px"
:min="0.01"
v-model:value="form.dealerPrice"
/>
<a-checkbox v-model:checked="form.dealerGift">有赠品</a-checkbox>
</a-space>
<div class="ele-text-placeholder">经销商价格</div>
</a-form-item>
<a-form-item label="进货价" name="buyingPrice">
<a-space>
<a-input-number
:placeholder="`进货价`"
:min="0.01"
style="width: 240px"
v-model:value="form.buyingPrice"
/>
</a-space>
<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="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="position">
<a-input
allow-clear
style="width: 250px"
placeholder="请输入货架"
v-model:value="form.position"
/>
</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";
import { GoodsCategory } from "@/api/shop/goodsCategory/model";
import { listGoodsCategory } from "@/api/shop/goodsCategory";
import { getMerchantId } from "@/utils/merchant";
// 是否是修改
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 takeaway = ref<GoodsCategory[]>([]);
const merchantId = getMerchantId();
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,
parentName: undefined,
categoryName: undefined,
specs: 0,
position: undefined,
price: undefined,
salePrice: undefined,
buyingPrice: undefined,
dealerPrice: undefined,
priceGift: undefined,
dealerGift: 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'
}
],
salePrice: [
{
required: true,
message: '请填写市场价',
type: 'number',
trigger: 'blur'
}
],
buyingPrice: [
{
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: GoodsCategory,value: any) => {
form.categoryId = value[1].value;
form.parentName = value[0].label;
form.categoryName = value[1].label;
}
const chooseTakeawayCategory = (item: GoodsCategory, value: any) => {
form.categoryParent = '店铺分类';
form.categoryChildren = value[0].label;
form.categoryId = item[0];
}
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 => {
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: 500,
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);
})
},
// 自定义文件上传(这里使用把选择的文件转成 blob 演示)
file_picker_callback: (callback: any, _value: any, meta: any) => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
// 设定文件可选类型
if (meta.filetype === 'image') {
input.setAttribute('accept', 'image/*');
} else if (meta.filetype === 'media') {
input.setAttribute('accept', 'video/*,.pdf');
}
input.onchange = () => {
const file = input.files?.[0];
if (!file) {
return;
}
if (meta.filetype === 'media') {
if (file.size / 1024 / 1024 > 200) {
editorRef.value?.alert({ content: '大小不能超过 200MB' });
return;
}
if(file.type.startsWith('application/pdf')){
uploadOss(file).then(res => {
const addPath = `<a href="${res.downloadUrl}" target="_blank">${res.name}</a>`;
content.value = content.value + addPath
})
return;
}
if (!file.type.startsWith('video/')) {
editorRef.value?.alert({ content: '只能选择视频文件' });
return;
}
uploadOss(file).then(res => {
callback(res.path)
});
}
};
input.click();
}
});
/* 粘贴图片上传服务器并插入编辑器 */
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,
category: JSON.stringify(category.value),
files: JSON.stringify(files.value),
goodsSpec: goodsSpec.value,
goodsSkus: skuList.value,
merchantId: getMerchantId(),
type: getMerchantId() ? 1 : 0
};
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.parentName){
category.value.push(props.data.parentName);
}
if(props.data.category){
category.value = JSON.parse(props.data.category);
}
if (props.data.content){
content.value = props.data.content;
}
// 外卖商品分类
listGoodsCategory({merchantId: props.merchantId}).then(list => {
takeaway.value = list
})
isUpdate.value = true;
} else {
spec.value = [];
goodsSpec.value = undefined;
skuList.value = [];
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>