mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-11 19:38:55 +08:00
feat(goods): 商品列表页新增批量导入弹窗
This commit is contained in:
@@ -4,8 +4,10 @@ import {
|
||||
postRequest,
|
||||
putRequest,
|
||||
deleteRequest,
|
||||
postRequestWithNoForm
|
||||
postRequestWithNoForm,
|
||||
uploadFileRequest
|
||||
} from "@/libs/axios";
|
||||
import { downloadBlob } from "@/utils/downloadBlob";
|
||||
// 获取数据字典
|
||||
export const getDictData = "/dictData/getByType/";
|
||||
// Websocket
|
||||
@@ -386,3 +388,23 @@ export const downLoadGoods = () => {
|
||||
export const uploadGoodsExcel = params => {
|
||||
return postRequestWithNoForm(`/goods/import/import`, params );
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载商品批量导入(多规格)模板(v2,同步文件流)
|
||||
* @returns {Promise<void>} 成功时直接触发浏览器下载「商品导入模板.xlsx」
|
||||
*/
|
||||
export const downloadGoodsImportTemplate = async () => {
|
||||
const blob = await getRequest('/goods/import/downLoad/v2', {}, 'blob');
|
||||
downloadBlob(blob, '商品导入模板.xlsx');
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量导入商品(多规格)Excel(v2)
|
||||
* @param {File} file 待导入的 .xlsx 文件,multipart 字段名 files
|
||||
* @returns {Promise<any>} 返回 {success, result: {successCount, failCount, failList: [{goodsNo, rows, reason}]}}
|
||||
*/
|
||||
export const importGoodsExcel = file => {
|
||||
const formData = new FormData();
|
||||
formData.append('files', file);
|
||||
return uploadFileRequest('/goods/import/import/v2', formData);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<!--
|
||||
商品批量导入(多规格v2)弹窗
|
||||
用途:商家端商品列表页批量导入商品;支持下载导入模板、上传 .xlsx 文件、
|
||||
展示导入结果(成功/失败计数及失败明细),失败明细可下载为 CSV。
|
||||
@author Bulbasaur
|
||||
@since 2026-10-20
|
||||
-->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="批量导入商品"
|
||||
width="560px"
|
||||
:close-on-click-modal="false"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<template v-if="!importResult">
|
||||
<p class="import-tip">
|
||||
请先下载导入模板,按模板格式填写商品信息(同一商品货号的多规格写为多行),仅支持 .xlsx 文件。
|
||||
</p>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
class="mb_10"
|
||||
:loading="templateLoading"
|
||||
@click="handleDownloadTemplate"
|
||||
>
|
||||
下载导入模板
|
||||
</el-button>
|
||||
<el-upload
|
||||
drag
|
||||
:show-file-list="false"
|
||||
accept=".xlsx"
|
||||
:disabled="importing"
|
||||
:before-upload="handleImportUpload"
|
||||
>
|
||||
<div class="el-upload__text" v-loading="importing">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<p>
|
||||
导入完成:成功 {{ importResult.successCount || 0 }} 条,失败
|
||||
{{ importResult.failCount || 0 }} 条
|
||||
</p>
|
||||
<el-table
|
||||
v-if="failList.length"
|
||||
:data="failList"
|
||||
max-height="320"
|
||||
border
|
||||
class="mt_10"
|
||||
>
|
||||
<el-table-column label="商品序号" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.goodsNo || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rows" label="行范围" width="110" align="center" />
|
||||
<el-table-column
|
||||
prop="reason"
|
||||
label="失败原因"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<template v-if="!importResult">
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button v-if="failList.length" @click="downloadFailDetail">
|
||||
下载失败明细
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleFinish">完成</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import {
|
||||
downloadGoodsImportTemplate,
|
||||
importGoodsExcel,
|
||||
} from "@/api/goods";
|
||||
import { downloadBlob } from "@/utils/downloadBlob";
|
||||
import { Message } from "@/utils/message";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue", "imported"]);
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
|
||||
const templateLoading = ref(false); // 模板下载 loading
|
||||
const importing = ref(false); // 导入请求 loading
|
||||
const importResult = ref(null); // 导入结果 {successCount, failCount, failList}
|
||||
const failList = computed(() => importResult.value?.failList || []);
|
||||
|
||||
// 下载导入模板
|
||||
function handleDownloadTemplate() {
|
||||
templateLoading.value = true;
|
||||
downloadGoodsImportTemplate()
|
||||
.then(() => {
|
||||
Message.success("模板下载成功");
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
templateLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 选择文件后立即上传(手动模式,before-upload 返回 false 阻止自动提交)
|
||||
function handleImportUpload(file) {
|
||||
if (!/\.xlsx$/i.test(file.name)) {
|
||||
Message.error("请上传 .xlsx 文件");
|
||||
return false;
|
||||
}
|
||||
importing.value = true;
|
||||
importGoodsExcel(file)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
importResult.value = res.result || {};
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
importing.value = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// CSV 字段转义:含逗号、引号或换行时用引号包裹
|
||||
function csvField(value) {
|
||||
const text = value == null ? "" : String(value);
|
||||
if (/[",\n\r]/.test(text)) {
|
||||
return `"${text.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// 将失败明细生成 CSV 落盘(加 BOM 保证 Excel 打开中文不乱码)
|
||||
function downloadFailDetail() {
|
||||
const header = "商品序号,行范围,失败原因";
|
||||
const lines = failList.value.map((item) =>
|
||||
[csvField(item.goodsNo || "-"), csvField(item.rows), csvField(item.reason)].join(",")
|
||||
);
|
||||
const csv = "\uFEFF" + [header, ...lines].join("\r\n");
|
||||
downloadBlob(csv, "商品导入失败明细.csv");
|
||||
}
|
||||
|
||||
// 完成:关闭弹窗并通知父页刷新列表
|
||||
function handleFinish() {
|
||||
dialogVisible.value = false;
|
||||
emit("imported");
|
||||
}
|
||||
|
||||
// 弹窗关闭后重置状态,下次打开重新走上传流程
|
||||
function handleClosed() {
|
||||
importResult.value = null;
|
||||
importing.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.import-tip {
|
||||
margin: 0 0 10px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.mb_10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.mt_10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -140,6 +140,9 @@
|
||||
>
|
||||
批量下架
|
||||
</el-button>
|
||||
<el-button type="primary" @click="importDialogVisible = true">
|
||||
批量导入
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@@ -252,6 +255,9 @@
|
||||
<el-button type="primary" :loading="submitLoading" @click="lower">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 商品批量导入(多规格v2) -->
|
||||
<GoodsImportDialog v-model="importDialogVisible" @imported="onGoodsImported" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -269,6 +275,7 @@ import {
|
||||
} from "@/api/goods";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import GoodsImportDialog from "./components/GoodsImportDialog.vue";
|
||||
|
||||
defineOptions({ name: "goods" });
|
||||
const emit = defineEmits(["edit-schedule", "delete-schedule"]);
|
||||
@@ -329,6 +336,7 @@ const currentStatus = ref(props.scheduledOnly ? props.scheduledStatus : "ALL");
|
||||
const goodsNumerData = ref({});
|
||||
const selectedRows = ref([]); // 选中的行数据
|
||||
const selectAll = ref(false); // 全选状态
|
||||
const importDialogVisible = ref(false); // 批量导入弹窗显示状态
|
||||
|
||||
const goodsStatusWithCount = computed(() => {
|
||||
return [
|
||||
@@ -525,6 +533,11 @@ function getNumberData() {
|
||||
}
|
||||
})
|
||||
}
|
||||
// 批量导入完成,刷新列表与状态数量
|
||||
function onGoodsImported() {
|
||||
getDataList();
|
||||
getNumberData();
|
||||
}
|
||||
// 编辑
|
||||
function edit(v) {
|
||||
id.value = v.id;
|
||||
|
||||
Reference in New Issue
Block a user