From b52d6c6e7a905985e9dec08346bc1a7dad717327 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Thu, 5 Mar 2026 14:21:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=89=8D=E7=AB=AF=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加批量上传API接口 - 扩展UploadData组件,添加批量上传弹窗 - 添加统计卡片展示(上传中、解析中、成功、失败) - 添加文件上传记录列表 - 实现轮询机制自动刷新状态 - 支持文件数量、格式、大小校验 - 支持手动刷新和状态筛选 - 添加响应式布局支持 --- ruoyi-ui/src/api/ccdiProjectUpload.js | 60 ++ .../components/detail/UploadData.vue | 633 +++++++++++++++++- 2 files changed, 690 insertions(+), 3 deletions(-) diff --git a/ruoyi-ui/src/api/ccdiProjectUpload.js b/ruoyi-ui/src/api/ccdiProjectUpload.js index 360268a..5f097f8 100644 --- a/ruoyi-ui/src/api/ccdiProjectUpload.js +++ b/ruoyi-ui/src/api/ccdiProjectUpload.js @@ -79,3 +79,63 @@ export function getImportStatus(taskId) { method: 'get' }) } + +// ========== 批量文件上传相关接口 ========== + +/** + * 批量上传文件 + * @param {Number} projectId 项目ID + * @param {Array} files 文件数组 + * @returns {Promise} 返回 batchId + */ +export function batchUploadFiles(projectId, files) { + const formData = new FormData() + files.forEach(file => { + formData.append('files', file) + }) + formData.append('projectId', projectId) + + return request({ + url: '/ccdi/file-upload/batch', + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + }, + timeout: 300000 // 5分钟超时 + }) +} + +/** + * 查询文件上传记录列表 + * @param {Object} params 查询参数 + */ +export function getFileUploadList(params) { + return request({ + url: '/ccdi/file-upload/list', + method: 'get', + params + }) +} + +/** + * 查询文件上传统计 + * @param {Number} projectId 项目ID + */ +export function getFileUploadStatistics(projectId) { + return request({ + url: `/ccdi/file-upload/statistics/${projectId}`, + method: 'get' + }) +} + +/** + * 查询文件上传详情 + * @param {Number} id 记录ID + */ +export function getFileUploadDetail(id) { + return request({ + url: `/ccdi/file-upload/detail/${id}`, + method: 'get' + }) +} diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue index d5ed886..abe8292 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue @@ -46,6 +46,126 @@ + +
+
+
+ +
+
+
上传中
+
{{ statistics.uploading }}
+
+
+ +
+
+ +
+
+
解析中
+
{{ statistics.parsing }}
+
+
+ +
+
+ +
+
+
解析成功
+
{{ statistics.parsed_success }}
+
+
+ +
+
+ +
+
+
解析失败
+
{{ statistics.parsed_failed }}
+
+
+
+ + +
+
+
+ + + + + + +
+ + 刷新 +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + +
将文件拖到此处,或点击上传
+
+ 支持 .xlsx, .xls 格式文件,最多100个文件,单个文件不超过50MB +
+
+ +
+
+ 已选择 {{ selectedFiles.length }} 个文件 +
+
+
+ + {{ file.name }} + {{ formatFileSize(file.size) }} + +
+
+
+ + + 取消 + 开始上传 + +
@@ -160,6 +337,9 @@ import { pullBankInfo, updateNameListSelection, uploadFile, + batchUploadFiles, + getFileUploadList, + getFileUploadStatistics, } from "@/api/ccdiProjectUpload"; export default { @@ -261,6 +441,34 @@ export default { level: "info", }, ], + // === 批量上传相关 === + batchUploadDialogVisible: false, + selectedFiles: [], + uploadLoading: false, + + // === 统计数据 === + statistics: { + uploading: 0, + parsing: 0, + parsed_success: 0, + parsed_failed: 0, + }, + + // === 文件列表相关 === + fileUploadList: [], + listLoading: false, + queryParams: { + projectId: null, + fileStatus: null, + pageNum: 1, + pageSize: 20, + }, + total: 0, + + // === 轮询相关 === + pollingTimer: null, + pollingEnabled: false, + pollingInterval: 5000, }; }, created() { @@ -272,6 +480,20 @@ export default { mounted() { // 组件挂载后监听项目ID变化 this.$watch("projectId", this.loadInitialData); + + // 加载统计数据和文件列表 + this.loadStatistics(); + this.loadFileList(); + + // 检查是否需要启动轮询 + this.$nextTick(() => { + if (this.statistics.uploading > 0 || this.statistics.parsing > 0) { + this.startPolling(); + } + }); + }, + beforeDestroy() { + this.stopPolling(); }, methods: { /** 加载初始数据 */ @@ -361,13 +583,19 @@ export default { const card = this.uploadCards.find((c) => c.key === key); if (!card) return; - if (key === "namelist") { - this.showNameListDialog = true; - } else { + if (key === "transaction") { + // 流水导入 - 打开批量上传弹窗 + this.batchUploadDialogVisible = true; + this.selectedFiles = []; + } else if (key === "credit") { + // 征信导入 - 保持现有逻辑 this.uploadFileType = key; this.uploadDialogTitle = `上传${card.title}`; this.uploadFileTypes = card.desc.replace(/.*支持|上传/g, "").trim(); this.showUploadDialog = true; + } else if (key === "namelist") { + // 名单库选择 - 保持现有逻辑 + this.showNameListDialog = true; } }, /** 文件选择变化 */ @@ -601,6 +829,223 @@ export default { }; return statusMap[status] || "未知"; }, + + // === 批量上传相关方法 === + + /** 批量上传的文件选择变化 */ + handleBatchFileChange(file, fileList) { + if (fileList.length > 100) { + this.$message.warning("最多上传100个文件"); + fileList = fileList.slice(0, 100); + } + + const validTypes = [".xlsx", ".xls"]; + const invalidFiles = fileList.filter((f) => { + const ext = f.name.substring(f.name.lastIndexOf(".")).toLowerCase(); + return !validTypes.includes(ext); + }); + + if (invalidFiles.length > 0) { + this.$message.error("仅支持 .xlsx, .xls 格式文件"); + return; + } + + const oversizedFiles = fileList.filter((f) => f.size > 50 * 1024 * 1024); + if (oversizedFiles.length > 0) { + this.$message.error("单个文件不能超过50MB"); + return; + } + + this.selectedFiles = fileList; + }, + + /** 删除已选文件 */ + handleRemoveFile(index) { + this.selectedFiles.splice(index, 1); + }, + + /** 开始批量上传 */ + async handleBatchUpload() { + if (this.selectedFiles.length === 0) { + this.$message.warning("请选择要上传的文件"); + return; + } + + this.uploadLoading = true; + + try { + const res = await batchUploadFiles( + this.projectId, + this.selectedFiles.map((f) => f.raw) + ); + + this.uploadLoading = false; + this.batchUploadDialogVisible = false; + + this.$message.success("上传任务已提交,请查看处理进度"); + + // 刷新数据并启动轮询 + await Promise.all([this.loadStatistics(), this.loadFileList()]); + + this.startPolling(); + } catch (error) { + this.uploadLoading = false; + this.$message.error("上传失败:" + (error.msg || "未知错误")); + } + }, + + // === 统计和列表相关方法 === + + /** 加载统计数据 */ + async loadStatistics() { + try { + const res = await getFileUploadStatistics(this.projectId); + this.statistics = res.data || { + uploading: 0, + parsing: 0, + parsed_success: 0, + parsed_failed: 0, + }; + } catch (error) { + console.error("加载统计数据失败:", error); + } + }, + + /** 加载文件列表 */ + async loadFileList() { + this.listLoading = true; + + try { + const params = { + projectId: this.projectId, + fileStatus: this.queryParams.fileStatus, + pageNum: this.queryParams.pageNum, + pageSize: this.queryParams.pageSize, + }; + + const res = await getFileUploadList(params); + this.fileUploadList = res.rows || []; + this.total = res.total || 0; + } catch (error) { + this.$message.error("加载文件列表失败"); + console.error(error); + } finally { + this.listLoading = false; + } + }, + + // === 轮询相关方法 === + + /** 启动轮询 */ + startPolling() { + if (this.pollingEnabled) return; + this.pollingEnabled = true; + + const poll = () => { + if (!this.pollingEnabled) return; + + Promise.all([this.loadStatistics(), this.loadFileList()]) + .then(() => { + if ( + this.statistics.uploading === 0 && + this.statistics.parsing === 0 + ) { + this.stopPolling(); + return; + } + this.pollingTimer = setTimeout(poll, this.pollingInterval); + }) + .catch((error) => { + console.error("轮询失败:", error); + this.pollingTimer = setTimeout(poll, this.pollingInterval); + }); + }; + + poll(); + }, + + /** 停止轮询 */ + stopPolling() { + this.pollingEnabled = false; + if (this.pollingTimer) { + clearTimeout(this.pollingTimer); + this.pollingTimer = null; + } + }, + + /** 手动刷新 */ + async handleManualRefresh() { + await Promise.all([this.loadStatistics(), this.loadFileList()]); + + this.$message.success("刷新成功"); + + if (this.statistics.uploading > 0 || this.statistics.parsing > 0) { + this.startPolling(); + } + }, + + // === 辅助方法 === + + /** 状态筛选 */ + handleStatusFilter(status) { + this.queryParams.fileStatus = status; + this.queryParams.pageNum = 1; + this.loadFileList(); + }, + + /** 分页变化 */ + handlePageChange(pageNum) { + this.queryParams.pageNum = pageNum; + this.loadFileList(); + }, + + /** 查看流水 */ + handleViewFlow(record) { + this.$emit("menu-change", { + key: "detail", + route: "detail", + params: { logId: record.logId }, + }); + }, + + /** 查看错误 */ + handleViewError(record) { + this.$alert(record.errorMessage || "未知错误", "错误信息", { + confirmButtonText: "确定", + type: "error", + }); + }, + + /** 状态文本映射 */ + getStatusText(status) { + const map = { + uploading: "上传中", + parsing: "解析中", + parsed_success: "解析成功", + parsed_failed: "解析失败", + }; + return map[status] || status; + }, + + /** 状态标签类型映射 */ + getStatusType(status) { + const map = { + uploading: "primary", + parsing: "warning", + parsed_success: "success", + parsed_failed: "danger", + }; + return map[status] || "info"; + }, + + /** 格式化文件大小 */ + formatFileSize(bytes) { + 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]; + }, }, }; @@ -876,6 +1321,97 @@ export default { } } +// 统计卡片区域 +.statistics-section { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 16px; + + .stat-card { + background: #fff; + border-radius: 4px; + padding: 20px; + display: flex; + align-items: center; + gap: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + cursor: pointer; + transition: all 0.3s; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + .stat-icon { + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + + &.uploading { + background: rgba(64, 158, 255, 0.1); + color: #409eff; + } + + &.parsing { + background: rgba(230, 162, 60, 0.1); + color: #e6a23c; + } + + &.success { + background: rgba(103, 194, 58, 0.1); + color: #67c23a; + } + + &.failed { + background: rgba(245, 108, 108, 0.1); + color: #f56c6c; + } + } + + .stat-content { + flex: 1; + + .stat-label { + font-size: 14px; + color: #909399; + margin-bottom: 4px; + } + + .stat-value { + font-size: 24px; + font-weight: 600; + color: #303133; + } + } + } +} + +// 文件列表区域 +.file-list-section { + background: #fff; + border-radius: 4px; + padding: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + + .list-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + + .filter-group { + display: flex; + gap: 12px; + } + } +} + // 上传弹窗样式 ::v-deep .el-dialog__wrapper { .upload-area { @@ -898,6 +1434,83 @@ export default { } } +// 批量上传弹窗样式 +.batch-upload-area { + width: 100%; + + ::v-deep .el-upload { + width: 100%; + + .el-upload-dragger { + width: 100%; + height: 180px; + } + } +} + +.selected-files { + margin-top: 16px; + border: 1px solid #ebeef5; + border-radius: 4px; + max-height: 300px; + overflow-y: auto; + + .files-header { + padding: 12px 16px; + background: #f5f7fa; + border-bottom: 1px solid #ebeef5; + font-size: 14px; + font-weight: 500; + color: #606266; + } + + .files-list { + padding: 8px; + + .file-item { + display: flex; + align-items: center; + padding: 8px 12px; + border-radius: 4px; + transition: background 0.3s; + + &:hover { + background: #f5f7fa; + } + + i { + font-size: 18px; + color: #1890ff; + margin-right: 8px; + } + + .file-name { + flex: 1; + font-size: 14px; + color: #303133; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .file-size { + font-size: 12px; + color: #909399; + margin: 0 12px; + } + + .el-button { + padding: 4px; + color: #909399; + + &:hover { + color: #f56c6c; + } + } + } + } +} + // 响应式 @media (max-width: 1200px) { .upload-section .upload-cards { @@ -908,6 +1521,10 @@ export default { grid-template-columns: repeat(3, 1fr); gap: 16px; } + + .statistics-section { + grid-template-columns: repeat(2, 1fr); + } } @media (max-width: 768px) { @@ -932,5 +1549,15 @@ export default { .quality-check-section .metrics { grid-template-columns: 1fr; } + + .statistics-section { + grid-template-columns: 1fr; + } + + .file-list-section .list-toolbar { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } }