451 lines
11 KiB
Vue
451 lines
11 KiB
Vue
<template>
|
||
<el-dialog title="导入产品" v-model="isShowDialog" width="500px" destroy-on-close @close="handleDialogClose">
|
||
<div class="import-container">
|
||
<!-- 顶部提示信息 - 只在没有文件时显示 -->
|
||
<div v-if="!hasFile" class="top-alert mb15">
|
||
<el-alert title="请先选择要上传的ZIP文件" type="warning" :closable="false" show-icon />
|
||
</div>
|
||
|
||
<!-- 文件上传区域 - 关键修复:添加 file-list 绑定 -->
|
||
<el-upload
|
||
ref="uploadRef"
|
||
class="upload-demo"
|
||
drag
|
||
:action="uploadAction"
|
||
:headers="uploadHeaders"
|
||
:data="uploadData"
|
||
:accept="acceptFileTypes"
|
||
:limit="1"
|
||
:on-success="handleUploadSuccess"
|
||
:on-error="handleUploadError"
|
||
:before-upload="beforeUpload"
|
||
:on-exceed="handleExceed"
|
||
:on-remove="handleRemove"
|
||
:on-change="handleFileChange"
|
||
:auto-upload="false"
|
||
:show-file-list="true"
|
||
:file-list="fileList"
|
||
>
|
||
<el-icon class="el-icon--upload">
|
||
<upload-filled />
|
||
</el-icon>
|
||
<div class="el-upload__text">将ZIP文件拖到此处,或<em>点击上传</em></div>
|
||
<template #tip>
|
||
<div class="el-upload__tip">支持 .zip 格式,文件大小不超过 {{ fileSizeLimit }}MB</div>
|
||
</template>
|
||
</el-upload>
|
||
|
||
<!-- 文件状态显示 -->
|
||
<div v-if="hasFile && currentFile" class="file-status mt15">
|
||
<el-alert :title="`文件准备就绪`" type="success" :closable="false" show-icon>
|
||
<div>文件已选择: {{ currentFile.name }} ({{ formatFileSize(currentFile.size) }})</div>
|
||
</el-alert>
|
||
</div>
|
||
|
||
<!-- 导入说明 -->
|
||
<div class="import-instructions mt20">
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>导入说明</span>
|
||
</div>
|
||
</template>
|
||
<div class="instruction-content">
|
||
<ul>
|
||
<li>1. 请先<el-link type="primary" @click="handleDownloadTemplate" :underline="false">下载导入模板</el-link>,查看数据格式</li>
|
||
<li>2. 按照模板格式准备您的产品数据</li>
|
||
<li>3. 将TXT文件打包成ZIP格式上传</li>
|
||
<li>4. 文件编码请使用UTF-8</li>
|
||
<li>5. 文件大小不能超过 {{ fileSizeLimit }}MB</li>
|
||
</ul>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button type="primary" :loading="downloadLoading" @click="handleDownloadTemplate" class="download-btn">
|
||
<template #icon>
|
||
<el-icon><Download /></el-icon>
|
||
</template>
|
||
{{ downloadLoading ? '生成中...' : '下载模板' }}
|
||
</el-button>
|
||
|
||
<el-button @click="handleCancel">取消</el-button>
|
||
|
||
<!-- 修复:使用更可靠的按钮禁用判断 -->
|
||
<el-button type="primary" :loading="loading" @click="handleSubmitUpload" :disabled="!canImport || loading">
|
||
{{ loading ? '导入中...' : '开始导入' }}
|
||
</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, reactive, nextTick, watch, computed } from 'vue';
|
||
import { ElMessage, ElMessageBox, type UploadInstance, type UploadFile, type UploadFiles } from 'element-plus';
|
||
import { UploadFilled, Download } from '@element-plus/icons-vue';
|
||
import JSZip from 'jszip';
|
||
import { saveAs } from 'file-saver';
|
||
import { importProduct } from '/@/api/customerService/product';
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'refresh'): void;
|
||
}>();
|
||
|
||
// 响应式状态
|
||
const isShowDialog = ref(false);
|
||
const loading = ref(false);
|
||
const downloadLoading = ref(false);
|
||
const uploadRef = ref<UploadInstance>();
|
||
const importResult = ref<any>(null);
|
||
const currentFile = ref<File | null>(null);
|
||
|
||
// 新增:显式管理文件列表
|
||
const fileList = ref<UploadFile[]>([]);
|
||
|
||
// 上传配置
|
||
const uploadAction = '/customerService/product/import';
|
||
const uploadHeaders = reactive({
|
||
Authorization: `Bearer ${localStorage.getItem('token') || ''}`,
|
||
});
|
||
const uploadData = reactive({});
|
||
const acceptFileTypes = '.zip,.ZIP';
|
||
const fileSizeLimit = 50;
|
||
|
||
// 修复:使用计算属性判断导入按钮状态
|
||
const canImport = computed(() => {
|
||
return fileList.value.length > 0 && fileList.value[0]?.status === 'ready';
|
||
});
|
||
|
||
const hasFile = computed(() => fileList.value.length > 0);
|
||
|
||
/**
|
||
* 处理文件变化事件 - 修复版本
|
||
*/
|
||
const handleFileChange = (file: UploadFile, files: UploadFile[]) => {
|
||
console.log('文件变化事件:', file.status, files.length);
|
||
|
||
// 更新文件列表
|
||
fileList.value = files;
|
||
|
||
if (file.status === 'ready') {
|
||
currentFile.value = file.raw;
|
||
ElMessage.success('文件已选择,可以开始导入');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理文件移除
|
||
*/
|
||
const handleRemove = (file: UploadFile, files: UploadFile[]) => {
|
||
fileList.value = files;
|
||
currentFile.value = null;
|
||
importResult.value = null;
|
||
ElMessage.info('文件已移除');
|
||
};
|
||
|
||
/**
|
||
* 手动提交上传 - 修复版本
|
||
*/
|
||
const handleSubmitUpload = async () => {
|
||
// 使用计算属性进行状态判断
|
||
if (!canImport.value || !currentFile.value) {
|
||
ElMessage.warning('请先选择有效的ZIP文件');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await ElMessageBox.confirm(`确认导入文件 "${currentFile.value.name}" 吗?`, '导入确认', {
|
||
confirmButtonText: '确认导入',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
});
|
||
|
||
loading.value = true;
|
||
|
||
// 直接使用当前文件进行上传
|
||
await handleCustomUpload(currentFile.value);
|
||
} catch (error) {
|
||
console.log('用户取消导入操作');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 上传前校验 - 优化版本
|
||
*/
|
||
const beforeUpload = (file: File): boolean => {
|
||
console.log('上传前校验:', file.name, file.size);
|
||
|
||
const isZip = file.type === 'application/zip' || file.name.toLowerCase().endsWith('.zip');
|
||
const isLtLimit = file.size / 1024 / 1024 < fileSizeLimit;
|
||
|
||
if (!isZip) {
|
||
ElMessage.error('请上传ZIP格式的压缩包文件!');
|
||
// 清除无效文件
|
||
setTimeout(() => {
|
||
if (uploadRef.value) {
|
||
uploadRef.value.clearFiles();
|
||
fileList.value = [];
|
||
}
|
||
}, 100);
|
||
return false;
|
||
}
|
||
|
||
if (!isLtLimit) {
|
||
ElMessage.error(`文件大小不能超过${fileSizeLimit}MB!`);
|
||
setTimeout(() => {
|
||
if (uploadRef.value) {
|
||
uploadRef.value.clearFiles();
|
||
fileList.value = [];
|
||
}
|
||
}, 100);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* 打开对话框 - 修复初始化
|
||
*/
|
||
const openDialog = () => {
|
||
isShowDialog.value = true;
|
||
loading.value = false;
|
||
downloadLoading.value = false;
|
||
importResult.value = null;
|
||
fileList.value = [];
|
||
currentFile.value = null;
|
||
|
||
nextTick(() => {
|
||
if (uploadRef.value) {
|
||
uploadRef.value.clearFiles();
|
||
}
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 处理对话框关闭
|
||
*/
|
||
const handleDialogClose = () => {
|
||
if (uploadRef.value) {
|
||
uploadRef.value.clearFiles();
|
||
}
|
||
fileList.value = [];
|
||
importResult.value = null;
|
||
currentFile.value = null;
|
||
};
|
||
|
||
// 其余函数保持不变(格式化文件大小、模板下载等)
|
||
const formatFileSize = (bytes: number): string => {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
};
|
||
|
||
const handleExceed = () => {
|
||
ElMessage.warning('只能上传一个文件,请先删除当前文件');
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
isShowDialog.value = false;
|
||
};
|
||
|
||
/**
|
||
* 自定义上传处理
|
||
*/
|
||
const handleCustomUpload = async (file: File) => {
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('type', 'zip');
|
||
formData.append('fileName', file.name);
|
||
|
||
const response = await importProduct(formData);
|
||
console.log('导入响应:', response);
|
||
|
||
if (response.code === 0 || response.failCount !== undefined) {
|
||
const resultData = response.data || response;
|
||
importResult.value = {
|
||
success: true,
|
||
data: resultData,
|
||
message: '导入成功',
|
||
};
|
||
|
||
ElMessage.success(`导入完成!失败记录:${resultData.failCount || 0}条`);
|
||
emit('refresh');
|
||
setTimeout(() => {
|
||
if (isShowDialog.value) {
|
||
isShowDialog.value = false;
|
||
}
|
||
}, 1000);
|
||
} else {
|
||
throw new Error(response.message || '导入失败');
|
||
}
|
||
} catch (error: any) {
|
||
console.error('导入失败:', error);
|
||
importResult.value = {
|
||
success: false,
|
||
message: error.message || '导入失败',
|
||
};
|
||
ElMessage.error(`导入失败:${error.message || '未知错误'}`);
|
||
}
|
||
};
|
||
|
||
// 模板下载相关函数保持不变
|
||
const createTextFile = (content: string, filename: string): Blob => {
|
||
const bom = new Uint8Array([0xef, 0xbb, 0xbf]);
|
||
const contentBytes = new TextEncoder().encode(content);
|
||
const blobContent = new Uint8Array(bom.length + contentBytes.length);
|
||
blobContent.set(bom);
|
||
blobContent.set(contentBytes, bom.length);
|
||
return new Blob([blobContent], { type: 'text/plain;charset=utf-8' });
|
||
};
|
||
|
||
const generateTemplateZip = async (): Promise<Blob> => {
|
||
try {
|
||
const zip = new JSZip();
|
||
const templateFolder = zip.folder('产品导入模板');
|
||
if (templateFolder) {
|
||
templateFolder.file('产品名称.txt', createTextFile('产品详情:', '产品名称.txt'));
|
||
// templateFolder.file('使用说明.txt', createTextFile('使用说明', '使用说明.txt'));
|
||
}
|
||
return await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });
|
||
} catch (error) {
|
||
throw new Error('模板生成失败');
|
||
}
|
||
};
|
||
|
||
const handleDownloadTemplate = async () => {
|
||
downloadLoading.value = true;
|
||
try {
|
||
const zipBlob = await generateTemplateZip();
|
||
const filename = `产品导入模板_${new Date().toISOString().split('T')[0].replace(/-/g, '')}.zip`;
|
||
saveAs(zipBlob, filename);
|
||
ElMessage.success(`模板下载成功!`);
|
||
} catch (error: any) {
|
||
ElMessage.error(`模板下载失败:${error.message || '未知错误'}`);
|
||
} finally {
|
||
downloadLoading.value = false;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 上传成功回调 - 手动上传模式下不会触发
|
||
*/
|
||
const handleUploadSuccess = (response: any, file: UploadFile, fileList: UploadFiles) => {
|
||
console.log('上传成功回调', response);
|
||
// 手动上传模式下不会执行到这里
|
||
};
|
||
|
||
/**
|
||
* 上传失败回调 - 手动上传模式下不会触发
|
||
*/
|
||
const handleUploadError = (error: Error, file: UploadFile, fileList: UploadFiles) => {
|
||
console.error('上传失败回调', error);
|
||
ElMessage.error('文件上传失败');
|
||
// 手动上传模式下不会执行到这里
|
||
};
|
||
|
||
defineExpose({ openDialog });
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 样式保持不变 */
|
||
.import-container {
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.top-alert.mb15 {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.upload-demo {
|
||
width: 100%;
|
||
}
|
||
|
||
.mt15 {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.mt20 {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.el-icon--upload {
|
||
font-size: 48px;
|
||
color: #c0c4cc;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.el-upload__text {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.el-upload__text em {
|
||
color: #409eff;
|
||
font-style: normal;
|
||
}
|
||
|
||
.el-upload__tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.file-status,
|
||
.import-instructions {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.instruction-content ul {
|
||
margin: 0;
|
||
padding-left: 16px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.instruction-content li {
|
||
margin-bottom: 4px;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.download-btn {
|
||
background-color: #67c23a;
|
||
border-color: #67c23a;
|
||
}
|
||
|
||
.download-btn:hover {
|
||
background-color: #5daf34;
|
||
border-color: #5daf34;
|
||
}
|
||
|
||
:deep(.el-upload-dragger) {
|
||
width: 100%;
|
||
height: 180px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
:deep(.el-upload-list) {
|
||
margin-top: 10px;
|
||
}
|
||
</style>
|