Files
admin-ui/src/views/customerService/product/component/importDialog.vue
2025-12-05 15:45:14 +08:00

451 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>