feat: 商品实体映射数据

This commit is contained in:
mixtan 2025-06-26 14:32:27 +08:00
parent 86e3825433
commit 0323cb0962
4 changed files with 938 additions and 0 deletions

113
src/api/goodsTool.js Normal file
View File

@ -0,0 +1,113 @@
import request from "@/utils/request";
import { stringify } from "qs";
export async function getProductMapperList(data) {
data = stringify(data);
return request({
url: `/admin/shop/shop-sync-productMapper/list?${data}`,
method: "get",
});
}
export async function getShopList() {
return request({
url: "/admin/shop/shop-store-base/list?pageNum=1&pageSize=99999&store_type=1",
method: "get",
});
}
export async function updateGoods(data) {
return request({
url: "/admin/shop/shop-sync-productMapper/udpateProductMapping",
method: "put",
data,
});
}
export async function deleteGoods(data) {
return request({
url: "/admin/shop/shop-sync-productMapper/delProductMapping",
method: "delete",
data,
});
}
export async function downloadTempGoods(data) {
data = stringify(data);
return request({
url: `/admin/shop/shop-sync-productMapper/template?${data}`,
method: "get",
});
}
export async function batchCreateGoods(data) {
return request({
url: "/admin/shop/shop-sync-productMapper/saveProductMappingBatch",
method: "post",
headers: {
'Content-Type': 'application/json',
},
data,
});
}
export async function batchExportGoods(data) {
return request({
url: "/admin/shop/shop-sync-productMapper/exportSelected",
method: "post",
data,
});
}
export async function getProductMapping(data) {
return request({
url: "/admin/shop/shop-sync-productMapper/getProductMapping",
method: "post",
data,
});
}
export async function getSyncBaseMapingProducts() {
return request({
url: "/admin/shop/shop-sync-productMapper/getSyncBaseMapingProducts",
method: "get",
});
}
export async function HandleDownloadErrorReport(data) {
data = stringify(data);
return request({
url: `/admin/shop/shop-sync-productMapper/download?${data}`,
method: "get",
});
}
export async function syncProductMaping() {
return request({
url: `/admin/shop/shop-sync-productMapper/syncProductMaping`,
method: "put",
});
}
export async function importGoodsData(data) {
return request({
url: `/admin/shop/shop-sync-productMapper/importData`,
method: "post",
data,
});
}
export default {
getProductMapperList,
getShopList,
updateGoods,
deleteGoods,
downloadTempGoods,
batchCreateGoods,
batchExportGoods,
getProductMapping,
getSyncBaseMapingProducts,
HandleDownloadErrorReport,
syncProductMaping,
importGoodsData,
};

View File

@ -13,6 +13,24 @@ import icon from '../views/403.vue'
*/
export function convertRouter(asyncRoutes) {
return asyncRoutes.map((route) => {
if (route.meta.title == '商品' && route.name == 'Vab320') {
const obj = {
path: '/goodsTool',
component: '@/views/product/goodsTool/index',
name: 'Vab88000',
redirect: null,
meta: {
title: '商品映射配置',
icon: '',
noClosable: 0,
hidden: null,
},
menuHidden: false,
}
route.children.splice(0, 0, obj)
}
if (route.meta.title == '店铺' && route.name == 'Vab330') {
const obj = {
path: '/shopAudit',

View File

@ -0,0 +1,411 @@
<template>
<div>
<el-drawer
:title="mode === 'add' ? '批量新增商品配置' : '编辑商品配置'"
:visible.sync="drawerVisible"
direction="rtl"
size="50%"
:before-close="handleClose"
>
<div class="fixed-header">
<el-button
type="primary"
icon="el-icon-plus"
v-if="mode === 'add'"
@click="addNewItem"
>添加组</el-button
>
</div>
<div class="scrollable-content">
<el-form :model="batchForm" label-width="80px" ref="batchFormRef">
<div
v-for="(item, index) in batchForm.items"
:key="index"
class="product-card"
>
<div class="card-header">
<span>{{ index + 1 }}</span>
<el-button
type="danger"
size="small"
v-if="mode === 'add' || batchForm.items.length > 1"
@click="removeItem(index)"
>删除</el-button
>
</div>
<div class="form-row">
<el-form-item
:prop="'items.' + index + '.storeId'"
:rules="rules.storeId"
label="店铺"
>
<el-select
style="width: 100%"
v-model="item.storeId"
placeholder="选择店铺"
filterable
clearable
>
<el-option
v-for="item in shopListData"
:key="item.store_id"
:label="item.store_name"
:value="item.store_id"
>
</el-option>
</el-select>
</el-form-item>
<el-form-item
:prop="'items.' + index + '.productName'"
:rules="rules.productName"
label="商品名称"
>
<el-input
v-model="item.productName"
placeholder="请输入商品名称"
></el-input>
</el-form-item>
</div>
<div class="form-row">
<el-form-item
:prop="'items.' + index + '.specValue'"
:rules="rules.specValue"
label="规格值"
>
<el-input
v-model.number="item.specValue"
type="number"
placeholder="请输入规格值"
></el-input>
</el-form-item>
</div>
<div class="form-row">
<el-form-item
:prop="'items.' + index + '.specUnit'"
:rules="rules.specUnit"
label="规格单位"
>
<el-input
v-model="item.specUnit"
placeholder="请输入规格单位"
></el-input>
</el-form-item>
<el-form-item
:prop="'items.' + index + '.sortOrder'"
:rules="rules.sortOrder"
label="排序"
>
<el-input
v-model.number="item.sortOrder"
type="number"
placeholder="请输入排序"
></el-input>
</el-form-item>
</div>
<div class="form-row">
<el-form-item
:prop="'items.' + index + '.description'"
:rules="rules.description"
label="描述"
>
<el-input
style="display: block"
v-model="item.description"
type="textarea"
:rows="1"
placeholder="请输入描述"
></el-input>
</el-form-item>
</div>
</div>
</el-form>
</div>
<div class="fixed-footer">
<el-button @click="drawerVisible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">{{
mode === "add" ? "提交" : "保存"
}}</el-button>
</div>
</el-drawer>
</div>
</template>
<script>
import GoodsToolApi from "@/api/goodsTool";
export default {
name: "BatchProductForm",
props: {
mode: {
type: String,
default: "add",
validator: (value) => ["add", "edit"].includes(value),
},
initialData: {
type: Array,
default: () => [],
},
shopListData: {
type: Array,
default: () => [],
},
},
data() {
return {
drawerVisible: false,
loading: false,
batchForm: {
items: [],
},
rules: {
productName: [
{ required: true, message: "请输入商品名称", trigger: "blur" },
],
storeId: [{ required: true, message: "请选择店铺", trigger: "change" }],
specValue: [
{ required: true, message: "请输入规格值", trigger: "blur" },
{ type: "number", message: "规格值必须为数字", trigger: "blur" },
],
specUnit: [
{ required: true, message: "请输入规格单位", trigger: "blur" },
],
sortOrder: [
{ required: true, message: "请输入排序", trigger: "blur" },
],
description: [
{ required: true, message: "请输入描述", trigger: "blur" },
],
},
};
},
watch: {
// props
initialData: {
handler(newVal) {
if (this.mode === "edit" && newVal && newVal.length > 0) {
//
this.batchForm.items = JSON.parse(JSON.stringify(newVal));
}
},
immediate: true,
deep: true,
},
},
methods: {
//
open() {
this.drawerVisible = true;
//
if (this.mode === "add" && this.batchForm.items.length === 0) {
this.addNewItem();
}
},
//
handleClose(done) {
if (this.hasUnsavedChanges()) {
this.$confirm("确定要关闭吗?未保存的数据将会丢失。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
done();
})
.catch(() => {
//
});
} else {
done();
}
},
//
hasUnsavedChanges() {
if (this.mode === "add") {
return this.batchForm.items.some(
(item) =>
item.productName || item.storeId || item.specValue || item.specUnit
);
}
//
if (this.initialData && this.initialData.length > 0) {
return (
JSON.stringify(this.batchForm.items) !==
JSON.stringify(this.initialData)
);
}
return false;
},
//
addNewItem() {
this.batchForm.items.push({
productName: "",
storeId: "",
specValue: "",
specUnit: "",
description: "",
sortOrder: "",
});
//
this.$nextTick(() => {
const container = document.querySelector(".scrollable-content");
if (container) {
container.scrollTop = container.scrollHeight;
}
});
},
//
removeItem(index) {
if (this.mode === "add" && this.batchForm.items.length > 1) {
console.log(111);
this.batchForm.items.splice(index, 1);
} else {
this.$message.warning("至少保留一个商品组配置");
}
},
//
submitForm() {
this.$refs.batchFormRef.validate(async(valid) => {
if (valid) {
this.loading = true;
//
const submitData = this.batchForm.items.map((item) => ({
productName: item.productName,
storeId: item.storeId,
specValue: item.specValue,
specUnit: item.specUnit,
description: item.description,
sortOrder: item.sortOrder,
}));
//
const filteredData =
this.mode === "add"
? submitData.filter(
(item) =>
item.productName ||
item.storeId ||
item.specValue ||
item.specUnit
)
: submitData;
if (filteredData.length === 0) {
this.$message.warning("请添加商品配置组数据");
this.loading = false;
return;
}
let res = null;
if(this.mode=='add'){
res = await GoodsToolApi.batchCreateGoods(submitData)
}else{
res = await GoodsToolApi.updateGoods({
...submitData?.[0],
id: this.batchForm.items?.[0].id
})
}
if(res.status==200){
this.$message.success("操作成功");
} else {
this.loading = false;
}
} else {
this.$message.error("请完善表单信息");
return false;
}
});
},
},
};
</script>
<style lang="scss" scoped>
.fixed-header {
padding: 0 12px 12px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: flex-end;
}
.scrollable-content {
padding: 12px;
height: calc(100vh - 195px);
overflow-y: auto;
}
.fixed-footer {
display: flex;
justify-content: flex-end;
padding: 12px;
border-top: 1px solid #ebeef5;
background-color: #fff;
}
.product-card {
width: 100%;
margin-bottom: 12px;
padding: 12px 12px 0;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
background-color: #fff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.form-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.form-row .el-form-item {
flex: 1 1 calc(33.33% - 12px);
margin-bottom: 0;
}
.form-row .el-form-item:last-child {
flex: 1 1 calc(33.34% - 12px);
}
.form-row .el-form-item:nth-child(3n) {
flex: 1 1 100%;
}
.form-row .el-input__inner {
height: 32px;
}
.form-row .el-input--textarea .el-input__inner {
height: auto;
}
</style>

View File

@ -0,0 +1,396 @@
<template>
<div class="container">
<div class="filter">
<el-input
class="input_item"
placeholder="输入商品名称"
prefix-icon="el-icon-search"
v-model="filter.productName"
clearable
></el-input>
<el-select
v-model="filter.storeId"
placeholder="选择店铺"
filterable
clearable
>
<el-option
v-for="item in shopList"
:key="item.store_id"
:label="item.store_name"
:value="item.store_id"
></el-option>
</el-select>
<el-button type="primary" size="medium" @click="getGoodsList">
搜索
</el-button>
</div>
<div class="list">
<div class="tool">
<el-button type="primary" @click="openBatchAdd">批量新增</el-button>
<el-button
type="success"
@click="handleExport"
:disabled="!selectedRowKeys.length"
>
导出数据
</el-button>
<el-upload
action=""
:show-file-list="false"
:limit="1"
:before-upload="beforeGoodsDataUpload"
>
<el-button type="warning">导入数据</el-button>
</el-upload>
<el-button type="danger" @click="HandleSyncGoods">同步数据</el-button>
<el-popover placement="bottom" :width="400" trigger="click">
<template slot="reference">
<el-button>下载错误报告</el-button>
</template>
<div>
<el-input v-model="errReportFilePath" placeholder="输入文件地址">
<template slot="append">
<el-button
type="primary"
size="medium"
@click="HandleDownloadErrorReport"
>
确定
</el-button>
</template>
</el-input>
</div>
</el-popover>
<el-button type="info" plain @click="handleSyncProductMaping">
自动计算并上架商品
</el-button>
</div>
<el-table
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
ref="productTable"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" />
<el-table-column prop="storeId" label="店铺ID" />
<el-table-column prop="productName" label="商品名称" />
<el-table-column prop="description" label="商品描述" />
<el-table-column prop="specValue" label="商品规格">
<template #default="scope">
{{ scope.row.specValue }}{{ scope.row.specUnit }}
</template>
</el-table-column>
<el-table-column label="操作" width="400">
<template #default="scope">
<el-button
size="mini"
type="primary"
plain
@click="editGoods(scope.row)"
>
编辑配置
</el-button>
<el-button
size="mini"
type="danger"
plain
@click="handleDelete(scope.row)"
>
删除配置
</el-button>
<el-button
size="mini"
type="info"
plain
@click="downloadTemplate(scope.row)"
>
下载模板
</el-button>
<el-button
size="mini"
type="success"
plain
@click="getProductMapping(scope.row)"
>
店铺商品映射
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:current-page="pagination.pageNum"
:page-size="pagination.pageSize"
:total="pagination.total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
<GoodsItem
ref="batchFormRef"
:mode="formMode"
:initialData="formData"
:shopListData="shopList"
@success="handleSuccess"
></GoodsItem>
</div>
</template>
<script>
import GoodsToolApi from '@/api/goodsTool'
import GoodsItem from './GoodsItem.vue'
export default {
components: {
GoodsItem,
},
data() {
return {
filter: {
productName: '',
storeId: '',
},
pagination: {
pageNum: 1,
pageSize: 10,
total: 0,
},
shopList: [],
tableData: [],
selectedRowKeys: [],
formMode: 'add',
formData: [],
apiUrl: '/api/products',
errReportFilePath: '',
}
},
mounted() {
this.getShopList()
this.getGoodsList()
},
methods: {
async getGoodsList() {
let res = await GoodsToolApi.getProductMapperList({
...this.filter,
pageNum: this.pagination.pageNum,
pageSize: this.pagination.pageSize,
})
this.tableData = res.data.items
this.pagination.total = res.data.records
},
async getShopList() {
let res = await GoodsToolApi.getShopList()
this.shopList = res.data.items
},
handleCurrentChange(val) {
this.pagination.pageNum = val
this.getGoodsList()
},
handleSizeChange(val) {
this.pagination.pageSize = val
this.getGoodsList()
},
openBatchAdd() {
this.formMode = 'add'
this.formData = []
this.$refs.batchFormRef.open()
},
openBatchEdit() {
const selectedProducts = this.tableData.filter((item) => item.selected)
if (selectedProducts.length === 0) {
this.$message.warning('请先选择要编辑的商品')
return
}
this.formMode = 'edit'
this.formData = selectedProducts
this.$refs.batchFormRef.open()
},
editGoods(data) {
this.formMode = 'edit'
this.formData = [data]
this.$refs.batchFormRef.open()
},
async getProductMapping(data) {
this.$confirm('确定要映射选中的商品配置吗?', '友情提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger',
}).then(async () => {
console.log(data)
const res = await GoodsToolApi.getProductMapping({ id: data.id })
if (res.status == 200) {
this.$message.success('操作成功')
this.getProductMapperList()
}
})
},
handleSuccess(data) {
this.fetchProductList()
},
handleSelectionChange(selection) {
this.selectedRowKeys = selection.map((item) => item.id)
},
handleDelete(data) {
this.$confirm(
'确定要删除选中的商品配置吗?此操作不可撤销。',
'友情提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger',
}
).then(async () => {
console.log(data)
const res = await GoodsToolApi.deleteGoods({ id: data.id })
if (res.status == 200) {
this.$message.success('已删除')
this.getProductMapperList()
}
})
},
HandleSyncGoods() {
this.$confirm('确定要获取同步未分配的商品数据?', '友情提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'danger',
}).then(async () => {
const res = await GoodsToolApi.getSyncBaseMapingProducts()
if (res.status == 200) {
this.$message.success('操作成功')
this.getProductMapperList()
}
})
},
async HandleDownloadErrorReport() {
if (!this.errReportFilePath) {
this.$message.warning('请输入文件地址')
return
}
const res = await GoodsToolApi.HandleDownloadErrorReport({
file: this.errReportFilePath,
})
if (res.status == 200) {
this.$message.success('操作成功')
this.downloadFile(res.data, `${this.errReportFilePath}.xlsx`)
}
},
async handleSyncProductMaping() {
const res = await GoodsToolApi.syncProductMaping()
if (res.status == 200) {
this.$message.success('操作成功')
}
},
async handleExport() {
if (!this.selectedRowKeys.length) {
this.$message.warning('请先选择要导出的商品')
return
}
const res = await GoodsToolApi.batchExportGoods(this.selectedRowKeys)
if (res.status == 200) {
this.downloadFile(response.data, '商品映射实体数据.xlsx')
}
},
async beforeGoodsDataUpload(file) {
const name = file.name.toLocaleLowerCase()
if (!name.endsWith('.xlsx') || !name.endsWith('.csv')) {
this.$message.error('文件格式错误仅支持xlsx或csv格式')
return
}
handleImportGoodsData(file)
},
async handleImportGoodsData(file) {
const data = new FormData()
data.append('file', file)
const res = await GoodsToolApi.importGoodsData(data)
if (res.status == 200) {
this.$message.success('操作成功')
}
},
async downloadTemplate(data) {
const res = await GoodsToolApi.downloadTempGoods({ id: data.id })
if (res.status == 200) {
this.$message.success('操作成功')
this.downloadFile(res, '商品商品映射数据模板.xlsx')
}
},
downloadFile(blobData, fileName) {
const url = window.URL.createObjectURL(new Blob([blobData]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
},
},
}
</script>
<style lang="scss" scoped>
.filter {
display: flex;
align-items: center;
padding: 20px;
margin: 20px;
gap: 10px;
border-radius: 5px;
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.1);
.input_item {
width: 250px;
}
}
.list {
padding: 20px;
margin: 20px;
display: flex;
flex-direction: column;
gap: 10px;
border-radius: 5px;
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.1);
}
.tool {
display: flex;
gap: 10px;
::v-deep .el-button {
margin-left: 0 !important;
}
}
</style>