使用vue + element Plus 实现商品添加规格
看效果图,分为两种但规格与多规格两种形式。
1.单规格
2.多规格
完整示例代码
从手工打造,写的不好多多指教,还有很大改善空间。
<template>
<div class="flex1">
<div class="right">
<el-card class="box-card mb15">
<p>商品规格</p>
<div class="typeRadio">
<el-radio v-model="data.radioType" :label="1">统一规格</el-radio>
<el-radio v-model="data.radioType" :label="2">多级规格</el-radio>
<span>*如有颜色、尺码等多种规格,请选择多级规格</span>
</div>
</el-card>
<!-- 统一规格 模块 -->
<el-card v-if="data.radioType == 1" class="box-card mb15">
<p>
价格库存
<el-tooltip effect="dark" content="当前页面全部为必填项" placement="right">
<i class="el-icon-question" style="font-size: 16px; margin-left: 5px"></i>
</el-tooltip>
</p>
<el-form ref="singleFormRef" label-position="left" :model="data.form" label-width="80px" :show-message="false">
<el-row :gutter="20">
<el-col :span="5">
<div class="grid-content bg-purple">
<el-form-item label="进价(¥)" prop="进价" :rules="{ required: true, trigger: 'change' }"
label-width="130px">
<el-input v-model="data.form.进价" placeholder="请输入" oninput="value=value.replace(/[^\d\.]/g,'')"
maxlength="8" show-word-limit></el-input>
</el-form-item>
</div>
</el-col>
<el-col :span="5">
<div class="grid-content bg-purple">
<el-form-item label="成本价(¥)" prop="成本价" :rules="[{ required: true, trigger: 'change' }]"
label-width="130px">
<el-input v-model="data.form.成本价" placeholder="请输入" oninput="value=value.replace(/[^\d\.]/g,'')"
maxlength="8" show-word-limit></el-input>
</el-form-item>
</div>
</el-col>
<el-col :span="5">
<div class="grid-content bg-purple">
<el-form-item label="最低售价(¥)" prop="销售价" :rules="[{ required: true, trigger: 'change' },
{ validator: (rule: any, value: any, callback: any) => {
const form = data.form;
if (Number(form.销售价) < Number(form.成本价)) {
callback('请输入销售价,且不能低于成本价');
} else {
return callback();
}
},
message: '请输入销售价,且不能低于成本价', trigger: 'blur' }]" label-width="130px">
<el-input type="text" v-model="data.form.销售价" placeholder="请输入"
oninput="value=value.replace(/[^\d\.]/g,'')" maxlength="8" autocomplete="no"
show-word-limit></el-input>
</el-form-item>
</div>
</el-col>
<el-col :span="5">
<div class="grid-content bg-purple">
<el-form-item label="市场价(¥)" prop="市场价" :rules="[{ required: true, trigger: 'change' },
{ validator: (rule: any, value: any, callback: any) => {
const form = data.form;
if (Number(form.市场价) < Number(form.销售价)) {
callback('请输入市场价,且不能低于销售价');
} else {
return callback();
}
}, message: '请输入市场价,且不能低于最低售价', trigger: 'blur'}]" label-width="130px">
<el-input type="text" v-model="data.form.市场价" placeholder="请输入"
oninput="value=value.replace(/[^\d\.]/g,'')" maxlength="8" autocomplete="no"
show-word-limit></el-input>
</el-form-item>
</div>
</el-col>
</el-row>
<!-- <el-row :gutter="20">-->
<!-- <el-col :span="5">-->
<!-- <el-form-item label="产品编码(选填)" label-width="auto">-->
<!-- <el-input v-model="data.form.party3sku" placeholder="产品编码" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- </el-row>-->
<el-form-item label="库存量( -1:表示库存充足 )" label-width="230px" prop="库存"
:rules="{ required: true, trigger: 'change' }">
<el-input-number
v-model="data.form.库存"
:min="-1"
controls-position="right"
size="large"
/>
</el-form-item>
</el-form>
</el-card>
<!-- 多规格 模块 -->
<el-card v-else class="box-card mb15">
<p>
添加商品规格
<el-tooltip effect="dark" content="当前页面全部为必填项" placement="right">
<i class="el-icon-question" style="font-size: 16px; margin-left: 5px"></i>
</el-tooltip>
</p>
<el-form ref="FormatFormRef" :model="data" :scroll-to-error="true" :show-message="false"
require-asterisk-position="left" label-position="left">
<div v-for="(item, i) in data.Format1" class="Format1Box" :key="item.index"
style="position: relative; padding: 0 16px; background-color: rgb(243 243 243); margin-bottom: 15px">
<i v-if="data.Format1.length > 1" @click="deleteFormat(i)" style="z-index: 9999; font-size: 28px; top: 10px"
class="el-icon-circle-close absolute show"></i>
<el-row :gutter="20">
<el-col :span="5">
<div class="grid-content bg-purple">
<el-form-item label="规格名" :id="'Format1.' + i + '.AttributeName'" label-width="130px"
:prop="'Format1.' + i + '.AttributeName'" :rules=" { required: true, trigger: 'blur' }">
<el-input v-model="item.AttributeName" placeholder="请输入" maxlength="20"
show-word-limit></el-input>
</el-form-item>
</div>
</el-col>
<!-- <el-col v-if="i == 0" :span="6">-->
<!-- <div class="grid-content bg-purple">-->
<!-- <el-form-item label="">-->
<!-- <el-checkbox-group v-model="item.type">-->
<!-- <el-checkbox label="展示规格图片" name="1" />-->
<!-- </el-checkbox-group>-->
<!-- </el-form-item>-->
<!-- </div>-->
<!-- </el-col>-->
</el-row>
<el-row :gutter="20" style="align-items: flex-end;">
<div v-for="(arrItem, itemIndex) in item.product_Attribute_Value_models" class="arrItem" :key="itemIndex"
style="position: relative; display: inline-block;">
<!-- {{itemIndex}}-->
<el-form-item style="margin-left: 10px" label-width="80" :label="itemIndex == 0 ? '规格值' : ''"
:prop="'Format1.' + i + '.product_Attribute_Value_models.' + itemIndex + '.product_Attribute_Value'"
:rules=" { required: true, trigger: 'blur' }">
<el-input class="input"
v-model="item.product_Attribute_Value_models[itemIndex].product_Attribute_Value"
placeholder="请输入" maxlength="20" show-word-limit></el-input>
<i v-if="item.product_Attribute_Value_models.length > 1"
@click="deleteValue(itemIndex, item.product_Attribute_Value_models, i)" style=" font-size: 18px"
class="el-icon-circle-close absolute arrItemShow"></i>
</el-form-item>
</div>
<div class="add" @click="addValue(i)">+ 添加规格值</div>
</el-row>
</div>
<div class="add" style="width: 100%" @click="addFormat">+ 添加商品规格</div>
<div class="space-between mt20">
<p>规格明细</p>
<p style="cursor: pointer;" @click="data.DialogVisible = true">批量填充</p>
</div>
<el-table class="table" :span-method="objectSpanMethod" :data="data.tableData" empty-text="暂无数据"
ref="elTableRef" style="width: 100%">
<el-table-column v-for="(item, index) in data.Format1" :key="index" min-height="20" align="left"
:prop="item.AttributeName" :label="item.AttributeName" min-width="auto">
<template #default="{ row }">
<div>
<span style="min-height: 40px;">{{ row.column[index]['product_Attribute_Value'] }}</span>
</div>
</template>
</el-table-column>
<el-table-column
prop="party3sku"
label="产品编码"
min-width="auto"
align="left"
>
<template #default="{ row }">
<el-input v-model="row.party3sku" placeholder="请输入"></el-input>
</template>
</el-table-column>
<el-table-column
prop="库存"
label="库存量"
min-width="auto"
align="left"
>
<template #header>
<span>库存量 </span>
<span style="color: red">*</span>
<el-tooltip effect="dark" content="库存为-1时表示充足" placement="right">
<i class="el-icon-question" style="font-size: 19px; margin-left: 5px"></i>
</el-tooltip>
</template>
<template #default="{ row }">
<el-form-item :prop="'tableData.' + row.Model_Value_orderID + '.库存'"
:rules=" { required: true, trigger: 'blur' }">
<el-input v-model="row.库存" placeholder="请输入" oninput="value=value.replace(/[^\d-]/g,'')"
maxlength="8"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column
prop="进价"
label="进价"
min-width="auto"
align="left"
>
<template #header>
<span>进价 </span>
<span style="color: red">*</span>
</template>
<template #default="{ row }">
<el-form-item :prop="'tableData.' + row.Model_Value_orderID + '.进价'"
:rules=" { required: true, trigger: 'blur' }">
<el-input v-model="row.进价" placeholder="请输入" oninput="value=value.replace(/[^\d\.]/g,'')"
maxlength="8"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column
prop="成本价"
label="成本价"
min-width="auto"
align="left"
>
<template #header>
<span>成本价 </span>
<span style="color: red">*</span>
</template>
<template #default="{ row }">
<el-form-item :prop="'tableData.' + row.Model_Value_orderID + '.成本价'"
:rules=" { required: true, trigger: 'blur' }">
<el-input v-model="row.成本价" placeholder="请输入" oninput="value=value.replace(/[^\d\.]/g,'')"
maxlength="8"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column
prop="销售价"
label="最低售价(¥)"
min-width="auto"
align="left"
>
<template #header>
<span>最低售价(¥) </span>
<span style="color: red">*</span>
</template>
<template #default="{ row }">
<el-form-item :prop="'tableData.' + row.Model_Value_orderID + '.销售价'"
:rules=" { required: true, trigger: 'blur' }">
<el-input v-model="row.销售价" placeholder="请输入" oninput="value=value.replace(/[^\d\.]/g,'')"
maxlength="8"></el-input>
</el-form-item>
</template>
</el-table-column>
<el-table-column
prop="市场价"
label="市场价"
min-width="auto"
align="left"
>
<template #header>
<span>市场价 </span>
<span style="color: red">*</span>
</template>
<template #default="{ row }">
<el-form-item :prop="'tableData.' + row.Model_Value_orderID + '.市场价'"
:rules=" { required: true, trigger: 'blur' }">
<el-input v-model="row.市场价" placeholder="请输入" oninput="value=value.replace(/[^\d\.]/g,'')"
maxlength="8"></el-input>
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
</el-card>
<el-button @click="cancelFormat">取消</el-button>
<el-button type="primary" @click="addProductAttribute">确定</el-button>
</div>
<el-dialog
v-model="data.DialogVisible"
title="批量填充"
width="500px"
destroy-on-close
style="border-radius: 8px;"
>
<span>
<el-form :model="data.dialogForm" label-width="80px" label-position="right">
<el-form-item label="库存量" label-position="left">
<el-input v-model="data.dialogForm.库存" oninput="value=value.replace(/[^\d-]/g,'')" maxlength="8"
show-word-limit/>
</el-form-item>
<el-form-item label="进价">
<el-input v-model="data.dialogForm.进价" maxlength="8" oninput="value=value.replace(/[^\d\.]/g,'')"
show-word-limit/>
</el-form-item>
<el-form-item label="成本价">
<el-input v-model="data.dialogForm.成本价" oninput="value=value.replace(/[^\d\.]/g,'')" maxlength="8"
show-word-limit/>
</el-form-item>
<el-form-item label="最低售价">
<el-input v-model="data.dialogForm.销售价" oninput="value=value.replace(/[^\d\.]/g,'')" maxlength="8"
show-word-limit/>
</el-form-item>
<el-form-item label="市场价">
<el-input v-model="data.dialogForm.市场价" oninput="value=value.replace(/[^\d\.]/g,'')" maxlength="8"
show-word-limit/>
</el-form-item>
</el-form>
</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.DialogVisible = false">取消</el-button>
<el-button type="primary" @click="batchAdd">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import {ref, reactive, toRefs, onMounted, nextTick, defineEmits, defineProps} from "vue";
import {
ProductModelDetail,
SaveProduct
} from "@/api/index";
const emit = defineEmits(["colseFormat"]);
import {ElMessage} from "element-plus";
const props = defineProps({
id: {type: String, required: true},
'Account_ID': {type: String, required: true},
});
// interface ObjData {
// name: any;
// index: number;
// type: Array<any>;
// arr: Array<string>;
// }
let p = '';
const FormatFormRef = ref(null);
const singleFormRef = ref(null);
let data: any = reactive({
radioType: 2,
// elTable: elTable,
num: 9999,
FormatFormRef: FormatFormRef,
singleFormRef: singleFormRef,
form: {},
active: 0,
DialogVisible: false,
dialogForm: {},
manyForm: {
type: []
},
tableData: [{
库存: '',
进价: '',
成本价: '',
销售价: '',
市场价: '',
column: [{
value: '',
index: 0,
name: '',
}]
}],
Format1: [
{
name: '',
index: 0,
type: [],
value: '',
AttributeName: '',
'product_Attribute_Value_models': [{
value: '',
index: 0,
name: '',
ID: ''
}]
}
]
});
/**
* 合并单元格
* @param row
* @param column
* @param rowIndex
* @param columnIndex
*/
const objectSpanMethod = ({
row,
column,
rowIndex,
columnIndex,
}: SpanMethodProps) => {
if (columnIndex < data.Format1.length) {
let rowspan = 1;
data.Format1.forEach((element: any, index: number) => {
if (index > columnIndex) {
rowspan = rowspan * (element.product_Attribute_Value_models ? element.product_Attribute_Value_models.length : 1);
}
});
if (rowIndex % rowspan === 0) {
return {
rowspan: rowspan,
colspan: 1,
};
} else {
return {
rowspan: 0,
colspan: 0,
};
}
}
};
const colseFormat = (params: any) => {
console.log(params);
emit("colseFormat", params);
};
/**
* 取消按钮
*/
const cancelFormat = () => {
colseFormat(false);
};
/**
* 规格名添加失去焦点事件
*/
const typeBlue = (e: any, i: number) => {
let obj = {[e.target.value]: ''};
let val = {[data.Format1[i].AttributeName]: ''};
};
/**
* 批量添加
*/
const batchAdd = () => {
data.tableData.forEach((item: any, i: number) => {
for (let key in data.dialogForm) {
item[key] = data.dialogForm[key];
}
});
data.dialogForm = {};
data.DialogVisible = false;
};
/**
* 数据转换 规格二维数组 转 排列组合一位数组
*/
const generateTableDatas = () => {
let types: any[][] = [];
data.Format1.forEach((element: { arr: Array<any> }, _index: any) => {
types.push(element.product_Attribute_Value_models);
});
let combine = function (chunks: any[][]) {
let res: Array<any[]> = [];
let helper = function (chunkIndex: number, prev: any[]) {
let chunk = chunks[chunkIndex];
let isLast = chunkIndex === chunks.length - 1;
for (let val of chunk) {
let cur: any[] = prev.concat(val);
if (isLast) {
// 如果已经处理到数组的最后一项了 则把拼接的结果放入返回值中
res.push(cur);
} else {
helper(chunkIndex + 1, cur);
}
}
};
// 从属性数组下标为 0 开始处理
// 并且此时的 prev 是个空数组
helper(0, []);
return res;
};
let datas = combine(types);
return datas;
};
/**
*
* @param i 添加规格值
*/
const addArrValue = (i: number) => {
let arr = data.Format1[i].product_Attribute_Value_models;
let obj = {
value: '',
ID: 0,
index: arr.length,
name: '',
'int_order': arr.length,
'product_Attribute_Value': '',
'product_Attribute_ID': data.Format1[i].ID
};
arr.push(obj);
};
/**
* 重置 table
*/
const setTable = (oldRowData: any) => {
data.tableData.splice(0);
let datas = generateTableDatas();
datas.forEach((_element: { arr: any }, _index: any) => {
data.tableData.push({
ID: 0,
库存: '',
进价: '',
成本价: '',
销售价: '',
'Model_Value': '',
市场价: '',
'Model_Value_orderID': _index,
'Account_ID': props.Account_ID,
'product_ID': props.id,
'product_title': '',
party3sku: '',
column: _element
});
});
data.tableData.forEach((tabItem: any, i: number) => {
let uid = '';
tabItem.column.forEach((column: any) => {
uid += column.index + '-';
});
tabItem['Model_Value_detail'] = uid;
if (oldRowData) {
const tableItemOld = oldRowData[uid];
for (let key in tableItemOld) {
if (key != 'column') {
tabItem[key] = tableItemOld[key];
}
}
tabItem['Model_Value_orderID'] = i;
tabItem['Model_Value_detail'] = uid;
}
});
};
/**
* 添加规格 值
*/
const addValue = (i: number) => {
let oldRowData: any = {};
data.tableData.forEach((tabItem: any) => {
let uid = '';
tabItem.column.forEach((column: any) => {
uid += column.index + '-';
});
oldRowData[uid] = tabItem;
});
addArrValue(i);
setTable(oldRowData);
};
/**
* 删除 规格 值
* @param i
* @param row
*/
const deleteValue = (i: number, row: Array<any>, pI: number) => {
row.splice(i, 1);
row.forEach((_element: any, _index: any) => {
_element.index = _index;
_element['int_order'] = _index;
});
// let datas = generateTableDatas();
let oldRowData: any = {};
data.tableData.forEach((tabItem: any) => {
let uid = '';
tabItem.column.forEach((column: any) => {
uid += column.index + '-';
});
oldRowData[uid] = tabItem;
});
setTable(oldRowData);
};
/**
* 添加商品规格
*/
const addFormat = () => {
let obj = {
ID: 0,
name: '',
AttributeName: '',
index: 1,
value: '',
type: [],
'Account_ID': props.Account_ID,
'org_ID': 0, // props.id,
"user_Group_ID": 327,
'product_Attribute_Value_models': []
};
data.Format1.push(obj);
addArrValue(data.Format1.length - 1);
setTable(null);
console.log(data.tableData);
};
/**
* 删除规格
* @param i
*/
const deleteFormat = (i: number) => {
deleteValue(i, data.Format1);
};
/**
* 获取商品规格
*/
const getProductModelDetail = async () => {
const {errorCode, dataTypes, dataNoAttribute, dataAttribute} = await ProductModelDetail({
'Account_shop_ID': 14889,
'Product_ID': props.id // props.id 80843
});
if (errorCode != 200) return false;
console.log(dataAttribute);
if (dataNoAttribute.length > 0) {
data.radioType = 1;
data.form = dataNoAttribute[0];
}
if (dataTypes.length > 0) {
data.radioType = 2;
let newAttribute = {};
dataAttribute.forEach((attr: any, index: number) => {
newAttribute[attr.Model_Value] = attr;
});
dataTypes.forEach((item: any, index: number) => {
// newAttribute
let Format = {
'Account_ID': props.Account_ID,
"user_Group_ID": 327,
'org_ID': 0, // props.id,
AttributeName: item.AttributeName,
name: item.AttributeName,
index: index,
type: [],
value: item.AttributeName,
ID: item.ID,
'product_Attribute_Value_models': []
};
let uid = '';
item.mv.forEach((mvItem: any, mvIndex: number) => {
let mv = {
value: mvItem.product_Attribute_Value,
index: mvIndex,
'int_order': mvIndex,
name: item.AttributeName,
ID: mvItem.ID,
'product_Attribute_Value': mvItem.product_Attribute_Value,
'product_Attribute_ID': item.ID,
};
Format.product_Attribute_Value_models.push(mv);
});
data.Format1.push(Format);
});
let datas = generateTableDatas();
datas.forEach((_element: any, _index: any) => {
let ModelValue = '';
let uid = '';
_element.forEach((item: any) => {
ModelValue += item.ID + '_';
uid += item.index + '-';
});
let obj = newAttribute[ModelValue];
data.tableData.push({
库存: obj?.库存,
进价: obj?.进价,
'Model_Value': ModelValue,
成本价: obj?.成本价,
销售价: obj?.销售价,
市场价: obj?.市场价,
ID: obj?.ID,
party3sku: obj?.party3sku,
'Model_Value_detail': uid,
'Model_Value_orderID': _index,
'product_ID': obj?.product_ID,
'product_title': '',
'Account_ID': props.Account_ID,
column: _element,
});
});
} else {
addFormat();
}
console.log(data.tableData);
};
const SaveProductReq = async (data: Array) => {
const {errorCode} = await SaveProduct(data);
if (errorCode != 200) {
ElMessage.error({
message: '提交失败,请稍后再试!',
type: "error",
});
} else {
ElMessage.success({
message: '保存成功!',
type: "success",
});
colseFormat(true);
}
};
/**
* 保存商品规格
*/
const addProductAttribute = () => {
let addList = {
// 'product_ID': props.id, // 商品 ID
'product_ID': props.id,
prodata: '',
types: '',
List: '',
};
if (data.radioType == 1) {
data.singleFormRef.validate((valid: any, object: any) => {
if (valid) {
addList.prodata = JSON.stringify(data.form);
SaveProductReq(addList);
}
});
} else {
data.FormatFormRef.validate(async (valid: any, object: any) => {
if (valid) {
let getTableData = JSON.parse(JSON.stringify(data.tableData));
getTableData.forEach((tabItem: any, tabIndex: number) => {
tabItem['product_ID'] = props.id;
let productTitle = "";
tabItem.column.forEach((SonItem: any, sonIndex: number) => {
if (sonIndex > 0) {
productTitle += ">" + SonItem.value;
} else {
productTitle += SonItem.value;
}
});
tabItem['product_title'] = productTitle;
delete tabItem.column;
});
addList.List = JSON.stringify(getTableData);
addList.types = JSON.stringify(data.Format1);
SaveProductReq(addList);
} else {
ElMessage.error({
message: '当前页面全部信息均为必填项',
type: "error",
});
// document.getElementById(Object.keys(object)[0]).scrollIntoView();
}
});
}
console.log(addList);
};
onMounted(() => {
data.Format1.splice(0);
data.tableData.splice(0);
getProductModelDetail();
// addFormat();
});
</script>
<style scoped lang="less">
.flex1 {
//display: flex;
.right {
//flex-grow: 1;
height: 100%;
overflow: auto;
.el-form {
width: 100%;
/deep/ .el-form-item {
display: flex;
flex-direction: column;
}
}
}
}
.box-card {
height: auto !important;
p {
margin: 5px 0 15px 0;
font-size: 15px;
height: auto;
}
.typeRadio {
span {
color: #999999;
}
}
/deep/ .el-form-item {
margin-bottom: 0;
}
}
.mb15 {
margin-bottom: 15px;
}
.mt20 {
margin-top: 20px;
}
/deep/ .el-button {
padding: 10px 40px;
}
.el-row {
align-items: self-end;
}
.add {
cursor: pointer;
border: #999999 dashed 1px;
width: 160px;
padding: 8px 0;
height: 16px;
line-height: 16px;
text-align: center;
display: inline-block;
margin: 0px 0 19px 0px;
}
.input {
margin: 0 20px 15px 0;
width: 260px
}
.space-between {
display: flex;
justify-content: space-between;
}
.table /deep/ .el-input__inner {
border: none;
//border-bottom: #fff 1px solid;
border-radius: 0;
padding-left: 0;
}
/deep/ .el-table__row {
min-height: 30px;
}
/deep/ .el-table__body {
min-height: 50px !important;
}
.absolute {
position: absolute;
right: 10px;
top: -6px;
color: #999;
display: none;
}
.Format1Box:hover .show {
display: block;
}
.arrItem:hover .arrItemShow {
display: block;
}
/deep/ .el-input-number__decrease {
border-bottom: 1px solid #cccccc;
line-height: 15px !important;
}
/deep/ .el-input-number__increase {
border-top: 1px solid #cccccc;
top: 3px !important;
}
</style>