From 0d14a3d3497f9f968e6a3c3177067aa6fe94a54c Mon Sep 17 00:00:00 2001 From: hongchao <3228015117@qq.com> Date: Wed, 10 Dec 2025 17:03:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AA=E7=BB=93=E7=AE=97=E6=8A=A5=E8=A1=A8zi?= =?UTF-8?q?p=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../material/cost/component/unreportHome.vue | 558 +++++++++++------- 1 file changed, 351 insertions(+), 207 deletions(-) diff --git a/src/views/material/cost/component/unreportHome.vue b/src/views/material/cost/component/unreportHome.vue index 85d98f3d..f04b40ce 100644 --- a/src/views/material/cost/component/unreportHome.vue +++ b/src/views/material/cost/component/unreportHome.vue @@ -38,17 +38,52 @@ 重置 导出Excel 查看历史报表 - {{ exporting ? '导出中...' : '批量导出ZIP' }} -
-
-
-
-
{{ progress }}% ({{ current }}/{{ totalZip }})
-
{{ statusText }}
-
+ + {{ exportButtonText }} + + +
+ + +
+ {{ progressText }} + {{ currentItem }}/{{ totalItems }} {{ currentFileName }} + + 速度: {{ formatBytes(downloadSpeed) }}/s + +
+
+ + 取消导出 + + + 完成 + +
+
+ 0 && !this.isExporting; + console.log('canDownload检查:', { + ids: this.ids.length, + isExporting: this.isExporting, + can: can + }); + return can; + }, + + /** 下载按钮文字 */ + exportButtonText() { + if (this.isExporting) { + return `导出中 ${this.progressPercentage}%`; + } + return `批量导出ZIP${this.ids.length > 0 ? ` (${this.ids.length})` : ''}`; + }, + }, created() { this.GetUnitData() this.GetProData() @@ -841,113 +903,221 @@ export default { //********************************************************************** */ //批量导出zip - // exportZip() { - // if (!this.ids.length) { - // this.$message.error('请选择要导出的记录') - // return false - // } - // let param = [] - // this.ids.map(item => { - // param.push({ agreementId: item.agreementId,settlementType:item.settlementType }) - // }) - // // exportLeaseAll(this.exportParams).then(res => { - // // downloadFile({ fileName: `月结明细_${new Date().getTime()}.zip`, fileData: res, fileType: 'application/zip;charset=utf-8' }) - // // }) - // this.downloadZip( - // 'material/slt_agreement_info/exportUnsettled', - // JSON.stringify(param), - // `未结算批量明细导出_${new Date().getTime()}.zip`, - // { background: true, showLoading: false, timeout: 600000 } - // ) - - // this.$refs.tableRef.clearSelection() - // }, - async exportZip() { if (!this.ids.length) { this.$message.error('请选择要导出的记录') return false } - let param = [] - this.ids.map(item => { - param.push({ agreementId: item.agreementId,settlementType:item.settlementType }) - }) + // 确认对话框 try { - this.exporting = true - this.progress = 0 - this.current = 0 - this.totalZip = 0 - this.statusText = '开始导出...' - - - const response = await getExportZipUnsettled(param) - const result = response - this.taskId = result.taskId - this.totalZip = result.total - this.statusText = '导出任务已开始...' - this.exportFlag = true - // 开始轮询进度 - this.startPolling() - } catch (error) { - console.error('导出请求失败', error) - this.statusText = '导出请求失败' - this.exporting = false + await this.$confirm(`确定要导出 ${this.ids.length} 个选中项吗?`, '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning', + center: true + }); + } catch (cancel) { + console.log('用户取消导出'); + return; } - this.$refs.tableRef.clearSelection() - }, - startPolling() { - this.pollInterval = setInterval(async () => { - try { - const param = { taskId: this.taskId } - const response = await getExportZipProgress(param) - const progress = response - this.progress = progress.percentage - this.current = progress.current - - if (progress.status === 'completed') { - this.statusText = '导出完成,开始下载...' - clearInterval(this.pollInterval) - this.downloadFile() - this.exporting = false - this.exportFlag = false - } else if (progress.status === 'error') { - this.statusText = `导出失败: ${progress.errorMsg}` - clearInterval(this.pollInterval) - this.exporting = false - } else { - this.statusText = `处理中: ${progress.percentage}%` - } - } catch (error) { - console.error('查询进度失败', error) - this.statusText = '查询进度失败' - this.exportFlag = false - } - }, 5000) - }, - - downloadFile() { - if (this.taskId) { - console.log("xxxxxxxxxxtaskId",this.taskId) - const param = { taskId: this.taskId } - this.downloadZip( - 'material/slt_agreement_info/dlExProgress', - JSON.stringify(param), - `未结算批量明细导出_${new Date().getTime()}.zip`, - ) + // 初始化下载状态 + this.initializeDownload(); + try { + let param = [] + this.ids.map(item => { + param.push({ agreementId: item.agreementId,settlementType:item.settlementType }) + }) + this.totalItems = param.length; + + const taskId = crypto.randomUUID() + const payload = { + list: param, + taskId:taskId, + zipName: `未结算批量明细导出_${new Date().getTime()}`, + stream: true + }; + + // 开始下载 + await this.streamDownload(payload); + this.$refs.tableRef.clearSelection() + } catch (err) { + console.error('未结算批量明细导出失败', err); + this.handleDownloadError(err); } }, - - beforeUnmount() { - if (this.pollInterval) { - clearInterval(this.pollInterval) + + /** 初始化下载状态 */ + initializeDownload() { + this.isExporting = true; + this.showProgress = true; + this.downloadComplete = false; + this.progressPercentage = 0; + this.currentItem = 0; + this.totalItems = 0; + this.currentFileName = ''; + this.progressText = '准备导出...'; + this.progressStatus = null; + this.downloadSpeed = 0; + this.retryCount = 0; + this.downloadedBytes = 0; + this.downloadStartTime = Date.now(); + this.downloadController = new AbortController(); + }, + + /** 流式下载方法 - 使用原始的request方式 */ + async streamDownload(payload) { + try { + this.progressText = '正在连接服务器...'; + + // 使用原始的request方法 + const response = await request({ + url: '/material/slt_agreement_info/dlExProgress', + method: 'POST', + data: payload, + responseType: 'blob', + timeout: 0, + onDownloadProgress: (progressEvent) => { + this.handleDownloadProgress(progressEvent); } + }); + + // 创建下载链接 + const blob = new Blob([response]); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = `${payload.zipName}.zip`; + document.body.appendChild(a); + a.click(); + + // 清理 + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + // 完成状态 + this.handleDownloadComplete(); + + } catch (error) { + if (error.message && error.message.includes('canceled')) { + this.progressText = '导出已取消'; + this.progressPercentage = 0; + this.progressStatus = 'exception'; + } else { + console.error('导出失败:', error); + this.handleDownloadError(error); + } + } + }, + + /** 处理下载进度 */ + handleDownloadProgress(progressEvent) { + if (progressEvent.total) { + // ZIP 文件流阶段 + const percentage = Math.round((progressEvent.loaded * 100) / progressEvent.total); + this.progressPercentage = Math.min(percentage, 99); + + const elapsed = Date.now() - this.downloadStartTime; + if (elapsed > 0) { + this.downloadSpeed = progressEvent.loaded * 1000 / elapsed; + this.progressText = `${this.formatBytes(progressEvent.loaded)} / ${this.formatBytes(progressEvent.total)}`; + } + } else { + // 后端处理文件阶段(无进度) + + // ⭐⭐ 1. 下载中保证 currentItem 不超过总数 + if (this.totalItems > 0) { + this.currentItem = Math.min(this.currentItem + 1, this.totalItems); + } + + // ⭐ 2. 百分比始终不超过 100 + this.progressPercentage = Math.min( + Math.round((this.currentItem / this.totalItems) * 100), + 100 + ); + + this.progressText = `处理中:${this.currentItem}/${this.totalItems}`; + } }, + + /** 处理下载完成 */ + handleDownloadComplete() { + // ⭐⭐ 2. 下载完成时补齐 + this.currentItem = this.totalItems; + this.progressPercentage = 100; + + this.progressText = '导出完成'; + this.progressStatus = 'success'; + this.downloadComplete = true; + this.isExporting = false; + this.downloadSpeed = 0; + + this.$message({ + message: '导出完成!', + type: 'success', + duration: 3000 + }); + }, + + + /** 处理下载错误 */ + handleDownloadError(error) { + console.error('导出失败:', error); + + if (this.retryCount < this.maxRetries) { + this.retryCount++; + this.progressText = `导出失败,正在重试 (${this.retryCount}/${this.maxRetries})...`; + this.progressStatus = 'warning'; + + // 3秒后重试 + setTimeout(() => { + this.exportZip(); + }, 3000); + } else { + this.progressText = '导出失败,请重试'; + this.progressStatus = 'exception'; + this.isExporting = false; + this.$message.error('导出失败: ' + (error.message || '未知错误')); + } + }, + + /** 取消下载 */ + cancelDownload() { + if (this.downloadController) { + this.downloadController.abort(); + } + this.isExporting = false; + this.showProgress = false; + this.$message.info('导出已取消'); + }, + + /** 清除进度显示 */ + clearProgress() { + this.showProgress = false; + this.downloadComplete = false; + this.progressPercentage = 0; + this.currentItem = 0; + this.totalItems = 0; + this.downloadSpeed = 0; + this.progressStatus = null; + this.currentFileName = ''; + }, + /** 格式化字节大小 */ + formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + }, + //********************************************************************** */ @@ -1707,41 +1877,97 @@ export default { } //导出zip +/* 进度条容器样式 */ .progress-container { - margin-top: 20px; - padding: 15px; + margin: 20px 0; + padding: 20px; border: 1px solid #e0e0e0; - border-radius: 4px; + border-radius: 8px; + background-color: #f9f9f9; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + animation: fadeIn 0.3s ease; } -.progress-bar { - width: 100%; - height: 20px; - background-color: #f0f0f0; - border-radius: 10px; - overflow: hidden; +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } -.progress-fill { - height: 100%; - background-color: #4CAF50; - transition: width 0.3s ease; +.progress-info { + margin-top: 15px; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; } .progress-text { - margin-top: 8px; - text-align: center; font-size: 14px; + color: #333; + font-weight: 500; +} + +.progress-detail { + font-size: 12px; color: #666; } -.status-text { - margin-top: 5px; - text-align: center; +.progress-speed { font-size: 12px; - color: #999; + color: #409eff; + font-weight: 500; } +.progress-actions { + margin-top: 15px; + display: flex; + justify-content: center; + gap: 10px; +} + +/* Element UI Progress 自定义样式 */ +::v-deep .el-progress-bar__inner { + background: linear-gradient(90deg, #409eff 0%, #67c23a 100%); + transition: width 0.3s ease; +} + +::v-deep .el-progress-bar__innerText { + color: #fff; + font-weight: bold; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); +} + +/* 响应式调整 */ +@media (max-width: 768px) { + .progress-container { + padding: 15px; + margin: 15px 0; + } + + .progress-text { + font-size: 13px; + } + + .progress-detail, + .progress-speed { + font-size: 11px; + } + + .el-button--mini { + min-width: 80px; + padding: 5px 12px; + font-size: 12px; + } +} + + + button:disabled { opacity: 0.6; cursor: not-allowed; @@ -1749,85 +1975,3 @@ button:disabled { // - - - \ No newline at end of file