diff --git a/src/views/material/report/reportQuery.vue b/src/views/material/report/reportQuery.vue index b0c27396..e6db8f79 100644 --- a/src/views/material/report/reportQuery.vue +++ b/src/views/material/report/reportQuery.vue @@ -100,50 +100,10 @@ 查询 重置 - - {{ exportButtonText }} - + 一键下载 - -
- - -
- {{ progressText }} - {{ currentItem }}/{{ totalItems }} {{ currentFileName }} - - 速度: {{ formatBytes(downloadSpeed) }}/s - -
-
- - 取消下载 - - - 完成 - -
-
@@ -341,6 +301,7 @@ @@ -372,13 +333,10 @@ import printJS from "print-js"; import QRCode from "qrcodejs2"; import axios from 'axios' import {downloadFile} from "@/utils/download"; -import request from "@/utils/request"; -import {getToken} from "@/utils/auth"; - export default { name: "ReportQuery", dicts: ['part_task_status'], - components: {vueEasyPrint}, + components: { vueEasyPrint }, data() { return { // 遮罩层 @@ -390,7 +348,7 @@ export default { // 显示搜索条件 showSearch: true, showHouse: false, - dateRange: [], + dateRange:[], ids: [], infos: [], // 总条数 @@ -405,14 +363,14 @@ export default { queryParams: { pageNum: 1, pageSize: 10, - keyWord: undefined, - taskStatus: undefined, - departName: undefined, - proName: undefined, - impUnitName: undefined, - typeName: undefined, - typeModelName: undefined, - jiJuType: undefined, + keyWord:undefined, + taskStatus:undefined, + departName:undefined, + proName:undefined, + impUnitName:undefined, + typeName:undefined, + typeModelName:undefined, + jiJuType:undefined, }, impUnitOptions: [], // 分公司下拉 departOptions: [], // 项目部下拉 @@ -426,68 +384,23 @@ export default { rowObj: {}, // 全局选中的项,用于跨页勾选 - selectedIds: [], - selectedData: {}, // 使用对象存储选中项数据,key为id - - // 下载进度相关数据 - isExporting: false, - showProgress: false, - downloadComplete: false, - progressPercentage: 0, - currentItem: 0, - totalItems: 0, - currentFileName: '', - progressText: '准备下载...', - progressStatus: '', - downloadSpeed: 0, - downloadController: null, - retryCount: 0, - maxRetries: 3, - downloadStartTime: 0, - downloadedBytes: 0, + selectedItems: new Map(), }; }, - computed: { - /** 是否可以下载 */ - canDownload() { - const can = this.selectedIds.length > 0 && !this.isExporting; - console.log('canDownload检查:', { - selectedIds: this.selectedIds.length, - isExporting: this.isExporting, - can: can - }); - return can; - }, - - /** 下载按钮文字 */ - exportButtonText() { - if (this.isExporting) { - return `下载中 ${this.progressPercentage}%`; - } - return `一键下载${this.selectedIds.length > 0 ? ` (${this.selectedIds.length})` : ''}`; - } - }, created() { - console.log('组件初始化'); this.getList(); this.getImpUnitOptions() this.departList() this.proList() this.getDeviceType() }, - beforeDestroy() { - // 清理资源 - if (this.downloadController) { - this.downloadController.abort(); - } - }, methods: { /** 获取分公司下拉 */ async getImpUnitOptions() { try { - const res = await getImpUnitListApi() + const res = await getImpUnitListApi() // 调后台接口 this.impUnitOptions = res.data.map(item => ({ - label: item.impUnitName, + label: item.impUnitName, // 这里根据实际字段替换 value: item.impUnitName })) } catch (e) { @@ -497,9 +410,9 @@ export default { /** 获取项目部下拉 */ async departList() { try { - const res = await getDepartListByImpUnitApi() + const res = await getDepartListByImpUnitApi() // 调后台接口 this.departOptions = res.data.map(item => ({ - label: item.departName, + label: item.departName, // 这里根据实际字段替换 value: item.departName })) } catch (e) { @@ -509,9 +422,9 @@ export default { /** 获取工程下拉 */ async proList() { try { - const res = await getProListByDepartApi() + const res = await getProListByDepartApi() // 调后台接口 this.proOptions = res.data.map(item => ({ - label: item.proName, + label: item.proName, // 这里根据实际字段替换 value: item.proName })) } catch (e) { @@ -520,22 +433,22 @@ export default { }, /** 分公司选择变化,加载项目部 */ async handleImpUnitChange() { - this.queryParams.departName = null - this.departOptions = [] - this.queryParams.proName = null - this.proOptions = [] + this.queryParams.departName = null // 清空项目部已选 + this.departOptions = [] // 清空原有下拉 + this.queryParams.proName = null // 清空工程已选 + this.proOptions = [] // 清空原有下拉 try { const params = { - impUnitName: this.queryParams.impUnitName, - departName: this.queryParams.departName, + impUnitName: this.queryParams.impUnitName, // 分公司名称 + departName: this.queryParams.departName, // 项目部名称 proName: this.queryParams.proName, - teamName: this.queryParams.teamName, - subUnitName: this.queryParams.subUnitName, + teamName:this.queryParams.teamName, + subUnitName:this.queryParams.subUnitName, } const res = await getDepartListByImpUnitApi(params) this.departOptions = res.data.map(item => ({ - label: item.departName, + label: item.departName, // 项目部名称字段 value: item.departName })) } catch (e) { @@ -544,20 +457,21 @@ export default { }, /** 项目部选择变化,加载工程 */ async handleDepartChange() { - this.queryParams.proName = null - this.proOptions = [] + this.queryParams.proName = null // 清空工程已选 + this.proOptions = [] // 清空原有下拉 try { + // 同时传入分公司和项目部参数 const params = { - impUnitName: this.queryParams.impUnitName, - departName: this.queryParams.departName, + impUnitName: this.queryParams.impUnitName, // 分公司名称 + departName: this.queryParams.departName, // 项目部名称 proName: this.queryParams.proName, - teamName: this.queryParams.teamName, - subUnitName: this.queryParams.subUnitName, + teamName:this.queryParams.teamName, + subUnitName:this.queryParams.subUnitName, } const res = await getProListByDepartApi(params) this.proOptions = res.data.map(item => ({ - label: item.proName, + label: item.proName, // 工程名称字段 value: item.proName })) } catch (e) { @@ -565,7 +479,7 @@ export default { } }, getDeviceType() { - getDeviceType({level: 3, skipPermission: 1}).then(response => { + getDeviceType({ level: 3, skipPermission: 1 }).then(response => { let matNameRes = response.data this.materialNameList = matNameRes.map((item) => { return { @@ -574,7 +488,7 @@ export default { } }) }) - getDeviceType({level: 4, skipPermission: 1}).then(response => { + getDeviceType({ level: 4, skipPermission: 1 }).then(response => { let matModelRes = response.data this.materialModelList = matModelRes.map((item) => { return { @@ -586,7 +500,7 @@ export default { }, // change设备类型 handleMaModel(e) { - this.queryParams.typeModelName = null + this.queryParams.typeModelName=null this.materialModelList = [] let typeId = null if (!e) { @@ -594,7 +508,8 @@ export default { } else { typeId = this.materialNameList.find(item => item.label == e).value } - getDeviceType({level: 4, skipPermission: 1, typeId}).then(response => { + console.log('🚀 ~ handleMaModel ~ typeId:', typeId) + getDeviceType({ level: 4, skipPermission: 1, typeId }).then(response => { let matModelRes = response.data this.materialModelList = matModelRes.map((item) => { return { @@ -607,12 +522,12 @@ export default { /** 查询列表 */ getList() { this.loading = true; - if (this.dateRange.length > 0) { - this.queryParams.startTime = this.dateRange[0] - this.queryParams.endTime = this.dateRange[1] - } else { - this.queryParams.startTime = undefined - this.queryParams.endTime = undefined + if(this.dateRange.length>0){ + this.queryParams.startTime=this.dateRange[0] + this.queryParams.endTime=this.dateRange[1] + }else{ + this.queryParams.startTime=undefined + this.queryParams.endTime=undefined } this.queryParams.taskStage = 3 getReportList(this.queryParams).then(response => { @@ -620,11 +535,11 @@ export default { this.total = response.data.total; this.loading = false; + // 加载完成后,根据全局选中状态设置当前页的选中项 this.$nextTick(() => { if (this.$refs.multipleTable) { - // 恢复之前选中的行 this.tableList.forEach(row => { - if (this.selectedIds.includes(row.id)) { + if (this.selectedItems.has(row.id)) { this.$refs.multipleTable.toggleRowSelection(row, true); } }); @@ -635,10 +550,10 @@ export default { /** 重置按钮操作 */ resetQuery() { this.resetForm("queryForm"); - this.dateRange = [] - this.queryParams.keyWord = null; - this.selectedIds = []; - this.selectedData = {}; + this.dateRange=[] + this.queryParams.keyWord=null; + // 清空选中状态 + this.selectedItems.clear(); if (this.$refs.multipleTable) { this.$refs.multipleTable.clearSelection(); } @@ -647,39 +562,41 @@ export default { /** 搜索按钮操作 */ handleQuery() { this.queryParams.pageNum = 1; - this.selectedIds = []; - this.selectedData = {}; + // 查询时清除选中状态 + this.selectedItems.clear(); if (this.$refs.multipleTable) { this.$refs.multipleTable.clearSelection(); } this.getList(); }, - // ==== 修复关键:简化的选中状态处理 ==== + // 多选框选中数据 handleSelectionChange(selection) { - console.log('选中项变化:', selection.length, '项'); - - // 清空现有选中状态 - this.selectedIds = []; - this.selectedData = {}; - - // 重新构建选中状态 + // 更新全局选中状态 selection.forEach(item => { - this.selectedIds.push(item.id); - this.selectedData[item.id] = item; + this.selectedItems.set(item.id, item); }); - console.log('当前选中ID:', this.selectedIds); - console.log('选中数据:', this.selectedData); + // 找出当前页未被选中但之前被选中的项,并从全局选中状态中移除 + const currentIds = selection.map(item => item.id); + for (let id of this.selectedItems.keys()) { + if (!currentIds.includes(id)) { + const itemInCurrentPage = this.tableList.find(row => row.id === id); + if (itemInCurrentPage) { + this.selectedItems.delete(id); + } + } + } - // 更新其他相关数据 - this.ids = this.selectedIds; - this.infos = selection.map(item => ({id: item.id})); + // 更新ids和infos数组,用于其他操作 + this.ids = Array.from(this.selectedItems.keys()); + this.infos = Array.from(this.selectedItems.values()).map(item => ({ id: item.id })); this.single = this.ids.length !== 1; this.multiple = this.ids.length === 0; }, //查看 - handleView(row) { - let query = {Id: row.id, taskId: row.taskId, isView: "true"} + handleView(row){ + console.log(row) + let query = { Id:row.id,taskId: row.taskId,isView:"true" } this.$tab.closeOpenPage({ path: '/part/partAcceptDetail', query, @@ -689,18 +606,25 @@ export default { handleFileView(url) { if (!url) return; + // 获取文件后缀名(忽略大小写) const fileExt = url.split('.').pop().toLowerCase(); + + // 定义不同类型的文件后缀 const imgExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; const pdfExts = ['pdf']; const docExts = ['doc', 'docx', 'xls', 'xlsx']; if (imgExts.includes(fileExt)) { - this.$viewerApi ? this.$viewerApi({images: [url]}) : window.open(url); + // 图片预览 + this.$viewerApi ? this.$viewerApi({ images: [url] }) : window.open(url); } else if (pdfExts.includes(fileExt)) { + // PDF 预览 window.open(url); } else if (docExts.includes(fileExt)) { + // Word、Excel 文件 → 下载 this.downloadFile(url); } else { + // 其他文件,默认下载 this.downloadFile(url); } }, @@ -718,22 +642,22 @@ export default { //查看验收单 async handleReport(row) { this.checkDataInfo = { - leaseProject: row.proName, - leaseUnit: row.departName + leaseProject:row.proName, + leaseUnit:row.departName } this.printTableData = [ { - typeName: row.typeName, - typeModelName: row.typeModelName, - unit: row.unit, - num: row.num, - maCode: row.maCode, - ratedLoad: row.ratedLoad, - testLoad: row.testLoad, - holdingTime: row.holdingTime, - testTime: row.testTime, - nextTestTime: row.nextTestTime, - checkResult: row.checkResult, + typeName:row.typeName, + typeModelName:row.typeModelName, + unit:row.unit, + num:row.num, + maCode:row.maCode, + ratedLoad:row.ratedLoad, + testLoad:row.testLoad, + holdingTime:row.holdingTime, + testTime:row.testTime, + nextTestTime:row.nextTestTime, + checkResult:row.checkResult, remark: '' } ] @@ -745,6 +669,13 @@ export default { }, //出库检验单打印 printCheck() { + // this.$refs.remarksPrintRefCheck.print() + // printJS({ + // printable: 'checkId1', + // type: 'html', + // targetStyles: ['*'] + // // 其他配置选项 + // }) this.$nextTick(() => { printJS({ printable: 'checkId1', @@ -767,32 +698,40 @@ export default { let context = canvas.getContext('2d') canvas.width = canvas.width context.height = canvas.height + // // 清除画布内容 + // context.clearRect(0, 0, canvas.width, canvas.height); + //let text = "XXX专用章"; + //let companyName = "XXX科技股份有限公司"; + // 绘制印章边框 let width = canvas.width / 2 let height = canvas.height / 2 context.lineWidth = 3 context.strokeStyle = '#f00' context.beginPath() - context.arc(width, height, 80, 0, Math.PI * 2) + context.arc(width, height, 80, 0, Math.PI * 2) //宽、高、半径 context.stroke() + //画五角星 this.create5star(context, width, height, 20, '#f00', 0) + // 绘制印章名称 context.font = '100 13px 宋体' - context.textBaseline = 'middle' - context.textAlign = 'center' + context.textBaseline = 'middle' //设置文本的垂直对齐方式 + context.textAlign = 'center' //设置文本的水平对对齐方式 context.lineWidth = 1 context.strokeStyle = '#ff2f2f' context.strokeText(text, width, height + 50) - context.translate(width, height) + // 绘制印章单位 + context.translate(width, height) // 平移到此位置, context.font = '100 13px 宋体' - let count = companyName.length - let angle = (4 * Math.PI) / (3 * (count - 1)) + let count = companyName.length // 字数 + let angle = (4 * Math.PI) / (3 * (count - 1)) // 字间角度 let chars = companyName.split('') let c for (let i = 0; i < count; i++) { - c = chars[i] + c = chars[i] // 需要绘制的字符 if (i == 0) { context.rotate((5 * Math.PI) / 6) } else { @@ -800,10 +739,10 @@ export default { } context.save() - context.translate(65, 0) - context.rotate(Math.PI / 2) - context.strokeStyle = '#ff5050' - context.strokeText(c, 0, 0) + context.translate(65, 0) // 平移到此位置,此时字和x轴垂直,公司名称和最外圈的距离 + context.rotate(Math.PI / 2) // 旋转90度,让字平行于x轴 + context.strokeStyle = '#ff5050' // 设置印章单位字体颜色为较浅的红色 + context.strokeText(c, 0, 0) // 此点为字的中心点 context.restore() } }, @@ -811,11 +750,14 @@ export default { create5star(context, sx, sy, radius, color, rotato) { context.save() context.fillStyle = color - context.translate(sx, sy) - context.rotate(Math.PI + rotato) - context.beginPath() + context.translate(sx, sy) //移动坐标原点 + context.rotate(Math.PI + rotato) //旋转 + context.beginPath() //创建路径 + // let x = Math.sin(0); + // let y = Math.cos(0); let dig = (Math.PI / 5) * 4 for (let i = 0; i < 5; i++) { + //画五角星的五条边 let x = Math.sin(i * dig) let y = Math.cos(i * dig) context.lineTo(x * radius, y * radius) @@ -827,14 +769,16 @@ export default { }, // 二维码查看 handleViewQrCode(row) { + console.log('🚀 ~ handleViewQrCode ~ row:', row) this.rowObj = row this.uploadOpen = true this.qrCode = row.qrCode let str = 'http://ahjj.jypxks.com:9988/imw/backstage/machine/qrCodePage?qrcode=' + row.qrCode + console.log('🚀 ~ handleViewQrCode ~ str:', str) this.$nextTick(() => { this.$refs.codeItem.innerHTML = '' var qrcode = new QRCode(this.$refs.codeItem, { - text: str, + text: str, //二维码内容 width: 256, height: 256, colorDark: '#000000', @@ -848,9 +792,9 @@ export default { const qrContainer = document.createElement('div') document.body.appendChild(qrContainer) - const qrSize = 512 - const padding = 20 - const fontSize = 68 + const qrSize = 512 // 放大二维码 + const padding = 20 // 边距也放大 + const fontSize = 68 // 放大文字 const maxTextWidth = qrSize const qrcode = new QRCode(qrContainer, { @@ -866,6 +810,7 @@ export default { const img = qrContainer.querySelector('img') || qrContainer.querySelector('canvas') const text = row.maCode || '' + // 计算换行 const ctxMeasure = document.createElement('canvas').getContext('2d') ctxMeasure.font = `${fontSize}px Arial` const words = text.split('') @@ -882,6 +827,7 @@ export default { } lines.push(line) + // 动态计算画布高度 const lineHeight = fontSize + 10 const qrCanvas = document.createElement('canvas') qrCanvas.width = qrSize + padding * 2 @@ -893,6 +839,7 @@ export default { ctx.drawImage(img, padding, padding, qrSize, qrSize) + // 绘制文字 ctx.fillStyle = '#000' ctx.font = `${fontSize}px Arial` ctx.textAlign = 'center' @@ -900,6 +847,7 @@ export default { ctx.fillText(ln, qrCanvas.width / 2, qrSize + padding + fontSize / 1.2 + index * lineHeight) }) + // 下载 const a = document.createElement('a') a.href = qrCanvas.toDataURL('image/png') a.download = text + '.png' @@ -910,6 +858,7 @@ export default { }, /** 单条下载 */ handleDownload(row) { + // 构建要传给后端的完整数据结构 const payload = { items: [ { @@ -922,53 +871,43 @@ export default { thirdReportUrl: row.thirdReportUrl || null, factoryReportUrl: row.factoryReportUrl || null, otherReportUrl: row.otherReportUrl || null, - unit: row.unit || '', - num: row.num || '', - maCode: row.maCode || '', - ratedLoad: row.ratedLoad || '', - testLoad: row.testLoad || '', - holdingTime: row.holdingTime || '', - testTime: row.testTime || '', - nextTestTime: row.nextTestTime || '', - checkResult: row.checkResult || '', + unit:row.unit || '', + num:row.num || '', + maCode:row.maCode || '', + ratedLoad:row.ratedLoad || '', + testLoad:row.testLoad || '', + holdingTime:row.holdingTime || '', + testTime:row.testTime || '', + nextTestTime:row.nextTestTime || '', + checkResult:row.checkResult || '', remark: '' }, ], zipName: row.proName || 'report', }; - this.downloadSingleFile(payload, `${row.proName || '文件档案下载'}.zip`); + this.downloadZip( + '/material/bm_report/downloadSingle', + JSON.stringify(payload), + `${row.proName || '文件档案下载'}.zip` + ); }, - /** 一键下载(多选) */ + + /** + * 一键下载(多选) + */ async handleBulkDownload() { - // 检查是否有选中项 - if (this.selectedIds.length === 0) { - this.$message.warning('请先勾选要下载的行'); - return; - } - - console.log('开始下载,选中数量:', this.selectedIds.length); - - // 确认对话框 + const grouped = {}; try { - await this.$confirm(`确定要下载 ${this.selectedIds.length} 个选中项吗?`, '提示', { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - center: true - }); - } catch (cancel) { - console.log('用户取消下载'); - return; - } + // 检查是否有选中项 + if (!this.selectedItems || this.selectedItems.size === 0) { + this.$message.warning('请先勾选要下载的行'); + return; + } - // 初始化下载状态 - this.initializeDownload(); - - try { - // 构建下载数据 - 直接从selectedData获取 - const items = Object.values(this.selectedData).map(item => ({ + // 从全局选中状态构建 items,不再只依赖当前页的 tableList + const items = Array.from(this.selectedItems.values()).map(item => ({ proName: item.proName || '', departName: item.departName || '', typeName: item.typeName || '', @@ -990,254 +929,54 @@ export default { remark: '' })); - console.log('构建了', items.length, '个下载项'); + // 按工程 -> 领用日期 -> 类型-规格分组 + items.forEach(item => { + const proName = item.proName || ""; + const leaseDate = item.testTime || ""; + const typeFolder = `${item.typeName || ""}-${item.typeModelName || ""}`; + + if (!grouped[proName]) grouped[proName] = {}; + if (!grouped[proName][leaseDate]) grouped[proName][leaseDate] = {}; + if (!grouped[proName][leaseDate][typeFolder]) grouped[proName][leaseDate][typeFolder] = []; + + grouped[proName][leaseDate][typeFolder].push(item); + }); + + // 拉平成后端需要的数组 + const flatItems = []; + Object.keys(grouped).forEach(proName => { + Object.keys(grouped[proName]).forEach(leaseDate => { + Object.keys(grouped[proName][leaseDate]).forEach(typeFolder => { + grouped[proName][leaseDate][typeFolder].forEach(item => { + // 可以把 leaseDate 覆盖到 item.testTime,这样后端就可以直接使用 + flatItems.push({ ...item, testTime: leaseDate }); + }); + }); + }); + }); - // 计算总文件数 - this.totalItems = this.calculateTotalFiles(items); - console.log('总文件数:', this.totalItems); - const taskId = crypto.randomUUID() const payload = { - items: items, - taskId:taskId, - zipName: `报告下载_${new Date().getTime()}`, - stream: true + items: flatItems, + zipName: `报告下载_${(new Date()).toISOString().slice(0,10)}`, }; - // 开始下载 - await this.streamDownload(payload); + this.downloadZip( + '/material/bm_report/downloadBulk', + JSON.stringify(payload), + payload.zipName + '.zip' + ); } catch (err) { console.error('一键下载失败', err); - this.handleDownloadError(err); + this.$message.error('一键下载失败'); } - }, - - /** 初始化下载状态 */ - 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(); - }, - - /** 计算总文件数 */ - calculateTotalFiles(items) { - let total = 0; - items.forEach(item => { - // 出库检验报告 - total += 1; - // 各个附件 - if (item.qualifiedUrl) total += 1; - if (item.testReportUrl) total += 1; - if (item.thirdReportUrl) total += 1; - if (item.factoryReportUrl) total += 1; - if (item.otherReportUrl) total += 1; - }); - return total; - }, - - /** 流式下载方法 - 使用原始的request方式 */ - /** 流式下载方法 - 使用原始的request方式 */ - async streamDownload(payload) { - try { - this.progressText = '正在连接服务器...'; - - const response = await request({ - url: '/material/bm_report/downloadBulkStream', - method: 'POST', - data: payload, - responseType: 'blob', - timeout: 0, - headers: { - encryptRequest: false, - checkIntegrity: false, - encryptResponse: false - }, - 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.handleBulkDownload(); - }, 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]; - }, - - /** 单文件下载 */ - async downloadSingleFile(payload, fileName) { - try { - this.isExporting = true; - - const response = await request({ - url: '/material/bm_report/downloadSingle', - method: 'post', - data: payload, - responseType: 'blob' - }); - - const blob = new Blob([response]); - const downloadUrl = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = downloadUrl; - link.download = fileName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - window.URL.revokeObjectURL(downloadUrl); - - this.$message.success('下载成功!'); - - } catch (error) { - console.error('下载失败:', error); - this.$message.error('下载失败'); - } finally { - this.isExporting = false; - } - }, } }; -