Files
guofu-admin/src/views/shop/demo/add/index.vue~merged_0
T
2024-10-19 10:51:09 +08:00

1051 lines
31 KiB
Plaintext

<template>
<a-page-header
:title="isUpdate ? '编辑商品' : '新增商品'"
@back="push('/goods/index')"
>
<div class="ele-cell ele-cell-align-top">
<div class="ele-cell-content">
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 3 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{
md: { span: 14 },
sm: { span: 24 },
xs: { span: 24 }
}"
>
<a-card title="基础信息" :bordered="false" class="goods-form">
<a-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { md: 24, sm: 24, xs: 24 } : { span: 8 }
"
>
<a-form-item label="选择店铺" name="merchantId" v-if="!form.merchantId">
<SelectMerchant
:placeholder="`选择商户`"
class="input-item"
v-model:value="form.merchantName"
@done="chooseMerchantId"
/>
</a-form-item>
<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="categoryId">
<SelectGoodsCategory
:data="data"
placeholder="请选择商品分类"
v-model:value="category"
@done="chooseGoodsCategory"
/>
<a-space style="margin-top: 10px">
<a @click="openUrl('/goods/category')">去新增</a>
</a-space>
</a-form-item>
<a-form-item
label="商品图片和视频"
name="images">
<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="goodsName">
<a-space style="margin-bottom: 20px">
<a-input
allow-clear
show-count
:maxlength="60"
style="width: 558px"
placeholder="请输入商品名称"
v-model:value="form.goodsName"
/>
</a-space>
</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-col>
</a-row>
</a-card>
<a-card title="规格售价与库存" :bordered="false" class="goods-form">
<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 label="商品价格" name="price" v-if="form.specs == 0">
<a-input-number :placeholder="`商品价格`" style="width: 180px" v-model:value="form.price" />
<div class="ele-text-placeholder">商品的实际购买金额,最低0.01</div>
</a-form-item>
<a-form-item label="划线价格" name="salePrice" v-if="form.specs == 0">
<a-input-number :placeholder="`划线价格`" style="width: 180px" v-model:value="form.salePrice" />
<div class="ele-text-placeholder">划线价仅用于商品页展示</div>
</a-form-item>
<a-form-item label="当前库存" name="stock" v-if="form.specs == 0">
<a-input-number :placeholder="`划线价格`" style="width: 180px" v-model:value="form.stock" />
<div class="ele-text-placeholder">商品的实际库存数量,为0时用户无法下单</div>
</a-form-item>
<!-- 商品多规格 -->
<a-form-item label="商品规格" name="skuList" v-if="form.specs == 1">
<template v-for="(item, index) in specList" :key="index">
<a-card class="spec-card">
<template #title>
<span style="margin-right: 10px">{{
`属性${index + 1}`
}}</span>
<SelectSpec
placeholder="请选择属性"
:width="130"
:specDict="specDict"
:index="index"
v-model:value="specList[index].specName"
@done="changeAttr"
/>
</template>
<template #extra>
<a @click="delAttribute(index)">删除属性</a>
</template>
<!--属性列表-->
<div class="spec-list">
<template v-for="(val,valIndex) in item.valueList" :key="valIndex">
<a-tag color="#108ee9" closable @close="onClose(index,valIndex)">{{ val.specValue }}</a-tag>
</template>
<SelectSpecValue
placeholder="请选择规格值"
:width="130"
:specId="item.specId"
:index="index"
@done="changeAttrValue"
/>
</div>
</a-card>
</template>
<a-card :bordered="false" v-if="specList.length < 3">
<a @click="addAttribute"
><PlusCircleOutlined style="margin-right: 4px" />创建新属性</a
>
</a-card>
skuList: {{ skuList }}
<!-- <a-divider />-->
<!-- {{ form.goodsSkus }}-->
<a-divider />
<!-- {{ skuColumns }}-->
<!-- 生生的sku数据 -->
<div class="sku-table">
<a-table
:pagination="false"
:dataSource="skuList"
:columns="skuColumns"
: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="chooseImageItem"
@del="onDeleteItem"
/>
</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>
<!-- {{ skuColumns }}-->
</div>
<div class="bath-set">
<!-- <h5 class="title">批量设置</h5>-->
<a-space class="list">
<a-input
:placeholder="`售卖价`"
v-model:value="bathSet.price"
/>
<a-input
:placeholder="`市场价`"
v-model:value="bathSet.salePrice"
/>
<a-input :placeholder="`库存`" v-model:value="bathSet.stock" />
<a-input
:placeholder="`规格编码`"
v-model:value="bathSet.skuNo"
/>
<a-button type="primary" @click="onBathSet">批量设置</a-button>
</a-space>
</div>
</a-form-item>
<a-form-item label="库存计算方式" name="deductStockType">
<a-radio-group v-model:value="form.deductStockType">
<a-radio :value="10">下单减库存</a-radio>
<a-radio :value="20">付款减库存</a-radio>
</a-radio-group>
</a-form-item>
</a-card>
<a-card title="商品详情" :bordered="false" class="goods-form">
<!-- 编辑器 -->
<tinymce-editor
ref="editorRef"
class="content"
v-model:value="content"
:disabled="disabled"
:init="config"
placeholder="图片直接粘贴自动上传"
@paste="onPaste"
/>
<div style="margin-top: 20px">
<SelectFile
:placeholder="`请选择视频文件`"
:limit="9"
:data="files"
@done="chooseFile"
@del="onDeleteFile"
/>
</div>
<div class="ele-text-placeholder"
>最多上传20个素材,拖拽可进行排序,单张图片需限制在10M以内</div
>
</a-card>
<a-card title="商品参数" :bordered="false" class="goods-form" />
<a-card title="更多设置" :bordered="false" class="goods-form" />
<a-card :bordered="false" class="goods-form">
<a-button type="primary" block size="large" @click="save">保存</a-button>
</a-card>
<div class="body-bottom"></div>
<a-card
:bordered="false"
class="goods-form"
style="position: fixed; bottom: 0; opacity: 0.9"
>
<!-- <div style="width: 100px">-->
<!-- <a-button block danger size="large" @click="save"-->
<!-- >删除</a-button-->
<!-- >-->
<!-- </div>-->
<div style="width: 100px">
<a-button type="primary" block size="large" @click="save">保存</a-button>
</div>
<!-- <div style="width: 100px">-->
<!-- <a-button type="primary" block size="large" @click="save"-->
<!-- >上架</a-button-->
<!-- >-->
<!-- </div>-->
</a-card>
</a-form>
</div>
<!-- 载入模拟器 -->
<Simulator :form="form" />
</div>
</a-page-header>
</template>
<script lang="ts" setup>
// 表单数据
import useFormData from '@/utils/use-form-data';
import { reactive, ref, unref, watch } from 'vue';
import { FormInstance, RuleObject } from 'ant-design-vue/es/form';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { Form, message } from 'ant-design-vue';
import { getDictionaryOptions, getMerchantId, openUrl } from "@/utils/common";
import { addGoods, updateGoods } from '@/api/shop/goods';
import { useRouter } from 'vue-router';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { BathSet, Goods } from '@/api/shop/goods/model';
import {PlusCircleOutlined} from '@ant-design/icons-vue';
import { getGoods } from '@/api/shop/goods';
import { listGoodsCategory } from '@/api/shop/goodsCategory';
import { FileRecord } from '@/api/system/file/model';
import {toTreeData, uuid} from 'ele-admin-pro';
import { ArticleCategory } from '@/api/cms/category/model';
import TinymceEditor from "@/components/TinymceEditor/index.vue";
import {uploadFile, uploadOss} from "@/api/system/file";
import {ColumnItem} from "ele-admin-pro/es/ele-pro-table/types";
import {listSpec} from '@/api/shop/spec';
import {Spec} from "@/api/shop/spec/model";
import { getMerchantName } from "@/utils/merchant";
import router from "@/router";
import { Merchant } from "@/api/shop/merchant/model";
const { currentRoute } = useRouter();
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const { push } = useRouter();
const useForm = Form.useForm;
const fileList = ref<any[]>([]);
const files = ref<ItemType[]>([]);
const imgList = ref<any[]>([]);
const specList = ref<any[]>([]);
const skuList = ref<any[]>([]);
const specDict = ref<any[]>([]);
const disabled = ref(false);
const addColumns = ref<ColumnItem[]>([]);
// 是否是修改
const isUpdate = ref(false);
const content = ref('');
const loading = ref(false);
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const category = ref<string[]>([]);
// 树形数据
const data = ref<ArticleCategory[]>([]);
// 树展开的key
const expandedRowKeys = ref<number[]>([]);
// 树选中的key
const selectedRowKeys = ref<number[]>([]);
// 选中数据
const current = ref<ArticleCategory | any>(null);
// 字典数据
const goodsAttr = getDictionaryOptions('goodsAttr');
const index = ref(0);
const { form, assignFields } = useFormData<Goods>({
goodsId: undefined,
type: 1,
goodsName: '',
image: '',
content: '',
code: '',
categoryId: undefined,
categoryName: '',
categoryParent: '',
categoryChildren: '',
specs: 0,
goodsSkus: [],
goodsSpecs: [],
position: undefined,
price: undefined,
salePrice: undefined,
sales: undefined,
stock: 1000,
deductStockType: 20,
files: '',
comments: '',
recommend: 0,
sortNumber: undefined,
status: undefined,
merchantName: '',
merchantId: getMerchantId()
});
const skuColumns = ref<ColumnItem[]>([
{
dataIndex: 'line',
key: 'line'
}
]);
const defaultColumns = ref<ColumnItem[]>([
{
title: '预览图',
dataIndex: 'image',
width: 120,
align: 'center',
key: 'image'
},
{
title: '商品价格',
dataIndex: 'price',
width: 120,
align: 'center',
key: 'price'
},
{
title: '划线价格',
dataIndex: 'salePrice',
width: 120,
align: 'center',
key: 'salePrice'
},
{
title: '库存数量',
dataIndex: 'stock',
width: 120,
align: 'center',
key: 'stock'
},
{
title: '规格编码',
dataIndex: 'skuNo',
width: 120,
align: 'center',
key: 'skuNo'
}
]);
const bathSet = ref<BathSet>({});
// 表单验证规则
const rules = reactive({
type: [
{
required: true,
message: '请选择商品类型',
type: 'number',
trigger: 'blur'
}
],
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 addAttribute = () => {
specList.value.push({
specId: 0,
specName: undefined,
valueList: []
})
};
const delAttribute = (index) => {
specList.value.splice(index,1);
skuColumns.value.splice(index,1);
};
const changeAttr = (item: Spec,index: number) => {
// 排重(未做)
specList.value[index].specId = item.specId;
specList.value[index].specName = item.value;
specList.value[index].valueList = [];
};
const changeAttrValue = (item: any, index: number) => {
specList.value[index].specId = item.specId;
// 添加属性
const length = specList.value[index].valueList.length;
specList.value[index].valueList.push({
specValueId: item.specValueId,
specValue: item.value,
specId: item.specId
})
// 组装表头
const arr = ref<any>([]);
specList.value.map((d) => {
arr.value.push({
title: d.specName,
dataIndex: `specValue${length}`,
key: `specValue${length}`,
width: 70,
align: 'center',
})
})
// 组装skuList
setTimeout(() => {
generateSku();
},200);
}
// 生成sku
const generateSku = () => {
skuColumns.value = [
{
dataIndex: 'line',
width: 50,
key: 'line'
}
];
// 根据选中的sku生成表格列
specList.value.forEach((item,index) => {
skuColumns.value.push({
title: item.specName,
dataIndex: `specValue${index}`,
key: `specValue${index}`,
align: 'center',
});
});
// 增加额外的列
const dataList = [
{
title: '商品图片',
dataIndex: 'image',
width: 120,
align: 'center',
key: 'image',
},
{
title: '价格',
dataIndex: 'price',
key: 'price',
width: 120,
align: 'center',
},
{
title: '市场价',
dataIndex: 'salePrice',
width: 120,
align: 'center',
key: 'salePrice'
},
{
title: '库存',
dataIndex: 'stock',
width: 120,
align: 'center',
key: 'stock'
},
{
title: 'SKU编码',
dataIndex: 'skuNo',
width: 120,
align: 'center',
key: 'skuNo'
}
];
dataList.forEach((item) => {
skuColumns.value.push({
title: item.title,
dataIndex: item.key,
key: item.key,
align: 'center',
});
});
// 组合规格值
const attrList = [];
specList.value.forEach((item, index) => {
attrList.push([]);
const propType = `specValue${index}`;
item.valueList.forEach((subItem) => {
attrList[index].push({
propValue: subItem.specValue,
propCode: null,
propType: propType
});
});
});
const attrs = differentCombinations(attrList);
// 将规格值加入到表格列中
attrs.forEach((itemList, index) => {
// skuList.value[index] = [];
itemList.forEach((subItem) => {
console.log(index)
console.log(subItem);
const key = subItem.propType;
const value = subItem.propValue;
skuList.value[index][key] = value;
skuList.value[index]['pirce'] = 0.1
});
});
};
// 生成sku函数
const differentCombinations = function (param) {
return param.reduce(
(a, b) => {
const ret = [];
a.forEach((i) => b.forEach((j) => ret.push(i.concat([j]))));
return ret;
},
[[]]
);
};
const onClose = (index: number, valIndex: number) => {
specList.value[index].valueList.splice(valIndex,1);
}
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.url;
};
const chooseImageItem = (data: FileRecord) => {
const index = data?.index;
skuList.value[index].images = []
skuList.value[index].images.push({
uid: data.id,
url: data.path,
status: 'done'
});
skuList.value[index].image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
};
const chooseFile = (data: FileRecord) => {
files.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
};
const onDeleteFile = (index: number) => {
files.value.splice(index, 1);
};
const chooseGoodsCategory = (item,value) => {
console.log(item);
console.log(value);
form.categoryId = value[1].value;
form.categoryParent = value[0].label;
form.categoryChildren = value[1].label;
}
/* 搜索 */
const chooseMerchantId = (item: Merchant) => {
form.merchantName = item.merchantName;
form.merchantId = item.merchantId;
};
const onBathSet = () => {
skuList.value.map(d => {
console.log(d);
d.price = bathSet.value.price;
d.salePrice = bathSet.value.salePrice;
d.stock = bathSet.value.stock;
d.skuNo = bathSet.value.skuNo;
})
}
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 reload = () => {
// 读取商品分类
listGoodsCategory()
.then((list) => {
loading.value = false;
const eks: number[] = [];
list.forEach((d) => {
d.key = d.categoryId;
d.value = d.categoryId;
if (typeof d.categoryId === 'number') {
eks.push(d.categoryId);
}
});
expandedRowKeys.value = eks;
data.value = toTreeData({
data: list.map((d) => {
d.disabled = d.type != 0;
return d;
}),
idField: 'categoryId',
parentIdField: 'parentId'
});
if (list.length) {
if (typeof list[0].categoryId === 'number') {
selectedRowKeys.value = [list[0].categoryId];
}
current.value = list[0];
} else {
selectedRowKeys.value = [];
current.value = null;
}
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
// 规格列表
listSpec({}).then(list => {
specDict.value = list.map((d,i) => {
return {
specId: d.specId,
name: d.specName,
value: d.specName,
<<<<<<< HEAD
list: d.specValues?.map(v => {
=======
list: d.specValue?.map(v => {
>>>>>>> origin/master
return {
specId: v.specId,
specValueId: v.specValueId,
name: v.specValue,
value: v.specValue
}
})
};
})
})
// 读取商品信息
if (form.goodsId) {
images.value = [];
files.value = [];
category.value = [];
content.value = '';
getGoods(form.goodsId).then((data) => {
assignFields(data);
if (data.image) {
const arr = JSON.parse(data.image);
arr.map((img) => {
images.value.push({
uid: uuid(),
url: img,
status: 'done'
});
});
}
if (data.files) {
const arr = JSON.parse(data.files);
arr.map((img) => {
files.value.push({
uid: uuid(),
url: img,
status: 'done'
});
});
}
if(data.content){
content.value = data.content;
}
if(data.goodsSpecs){
specList.value = data.goodsSpecs.map(d => {
d.valueList = [];
const split = d.specValues?.split(',');
split?.map(text => {
d.valueList?.push({
specValue: text
})
})
return d;
});
}
setTimeout(() => {
if(data.categoryParent){
console.log(category.value);
category.value.push(data.categoryParent);
}
if(data.categoryChildren){
category.value.push(data.categoryChildren);
}
if(data.goodsSkus){
skuList.value = data.goodsSkus;
generateSku();
}
},300)
});
}
};
const { validate, resetFields, validateInfos } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
images.value.map((d) => {
imgList.value.push(d.url);
});
files.value.map((d) => {
fileList.value.push(d.url);
});
skuList.value.map(d => {
d.images = undefined
})
if(getMerchantId()){
form.merchantId = getMerchantId();
form.merchantName = getMerchantName();
}
const formData = {
...form,
content: content.value,
image: JSON.stringify(imgList.value),
files: JSON.stringify(fileList.value),
goodsSpecs: specList.value,
goodsSkus: skuList.value
};
const saveOrUpdate = isUpdate.value ? updateGoods : addGoods;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
imgList.value = []
fileList.value = []
category.value = []
resetFields();
message.success(msg);
setTimeout(() => {
router.go(-1)
},1000)
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
currentRoute,
(route) => {
const { query } = unref(route);
const { goodsId } = query;
if (goodsId) {
form.goodsId = Number(goodsId);
isUpdate.value = true;
reload();
} else {
isUpdate.value = false;
imgList.value = []
fileList.value = []
// images.value = [];
// skuList.value = [];
// specList.value = [];
// category.value = [];
resetFields();
}
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'GoodsAdd'
};
</script>
<style lang="less" scoped>
.goods-form {
width: 1020px;
margin-bottom: 10px;
.spec-card {
margin-top: 20px;
background-color: #f8fcff;
.spec-list {
margin-left: 52px;
.spec-value-list {
margin-bottom: 10px;
display: flex;
align-items: center;
.image {
margin-top: 10px;
margin-left: 10px;
}
}
}
}
}
.body-bottom {
height: 100px;
}
.bath-set {
display: flex;
flex-direction: column;
margin-top: 16px;
.list {
width: 100%;
margin: 10px 0;
display: flex;
}
}
.sku-table {
.goods-list {
.head {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #eeeeee;
div {
padding: 6px;
width: 25%;
}
}
.table-td {
display: flex;
justify-content: space-between;
div {
width: 25%;
padding: 6px;
}
}
}
.desc {
margin-top: 20px;
padding: 10px 0;
}
}
.preview-ui {
position: fixed;
top: 155px;
right: 30px;
background: url('@/assets/img/app-ui.png');
background-repeat: no-repeat;
background-position: top;
background-size: 100%;
width: 390px;
height: 844px;
}
.ele-text-placeholder{
line-height: 2em;
}
</style>