diff --git a/src/views/material/report/reportQuery.vue b/src/views/material/report/reportQuery.vue
index 239022ff..f97f4f35 100644
--- a/src/views/material/report/reportQuery.vue
+++ b/src/views/material/report/reportQuery.vue
@@ -100,10 +100,50 @@
查询
重置
- 一键下载
+
+ {{ exportButtonText }}
+
+
+
+
+
+
+ {{ progressText }}
+ {{ currentItem }}/{{ totalItems }} {{ currentFileName }}
+
+ 速度: {{ formatBytes(downloadSpeed) }}/s
+
+
+
+
+ 取消下载
+
+
+ 完成
+
+
+
@@ -301,7 +341,6 @@
@@ -333,10 +372,13 @@ 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 {
// 遮罩层
@@ -348,7 +390,7 @@ export default {
// 显示搜索条件
showSearch: true,
showHouse: false,
- dateRange:[],
+ dateRange: [],
ids: [],
infos: [],
// 总条数
@@ -363,14 +405,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: [], // 项目部下拉
@@ -384,23 +426,68 @@ export default {
rowObj: {},
// 全局选中的项,用于跨页勾选
- selectedItems: new Map(),
+ 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,
};
},
+ 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) {
@@ -410,9 +497,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) {
@@ -422,9 +509,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) {
@@ -433,22 +520,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) {
@@ -457,21 +544,20 @@ 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) {
@@ -479,7 +565,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 {
@@ -488,7 +574,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 {
@@ -500,7 +586,7 @@ export default {
},
// change设备类型
handleMaModel(e) {
- this.queryParams.typeModelName=null
+ this.queryParams.typeModelName = null
this.materialModelList = []
let typeId = null
if (!e) {
@@ -508,8 +594,7 @@ export default {
} else {
typeId = this.materialNameList.find(item => item.label == e).value
}
- console.log('🚀 ~ handleMaModel ~ typeId:', typeId)
- getDeviceType({ level: 4, skipPermission: 1, typeId }).then(response => {
+ getDeviceType({level: 4, skipPermission: 1, typeId}).then(response => {
let matModelRes = response.data
this.materialModelList = matModelRes.map((item) => {
return {
@@ -522,12 +607,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 => {
@@ -535,11 +620,11 @@ export default {
this.total = response.data.total;
this.loading = false;
- // 加载完成后,根据全局选中状态设置当前页的选中项
this.$nextTick(() => {
if (this.$refs.multipleTable) {
+ // 恢复之前选中的行
this.tableList.forEach(row => {
- if (this.selectedItems.has(row.id)) {
+ if (this.selectedIds.includes(row.id)) {
this.$refs.multipleTable.toggleRowSelection(row, true);
}
});
@@ -550,10 +635,10 @@ export default {
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
- this.dateRange=[]
- this.queryParams.keyWord=null;
- // 清空选中状态
- this.selectedItems.clear();
+ this.dateRange = []
+ this.queryParams.keyWord = null;
+ this.selectedIds = [];
+ this.selectedData = {};
if (this.$refs.multipleTable) {
this.$refs.multipleTable.clearSelection();
}
@@ -562,41 +647,39 @@ export default {
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
- // 查询时清除选中状态
- this.selectedItems.clear();
+ this.selectedIds = [];
+ this.selectedData = {};
if (this.$refs.multipleTable) {
this.$refs.multipleTable.clearSelection();
}
this.getList();
},
- // 多选框选中数据
+ // ==== 修复关键:简化的选中状态处理 ====
handleSelectionChange(selection) {
- // 更新全局选中状态
+ console.log('选中项变化:', selection.length, '项');
+
+ // 清空现有选中状态
+ this.selectedIds = [];
+ this.selectedData = {};
+
+ // 重新构建选中状态
selection.forEach(item => {
- this.selectedItems.set(item.id, item);
+ this.selectedIds.push(item.id);
+ this.selectedData[item.id] = item;
});
-
- // 找出当前页未被选中但之前被选中的项,并从全局选中状态中移除
- 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);
- }
- }
- }
-
- // 更新ids和infos数组,用于其他操作
- this.ids = Array.from(this.selectedItems.keys());
- this.infos = Array.from(this.selectedItems.values()).map(item => ({ id: item.id }));
+
+ console.log('当前选中ID:', this.selectedIds);
+ console.log('选中数据:', this.selectedData);
+
+ // 更新其他相关数据
+ this.ids = this.selectedIds;
+ this.infos = selection.map(item => ({id: item.id}));
this.single = this.ids.length !== 1;
this.multiple = this.ids.length === 0;
},
//查看
- handleView(row){
- console.log(row)
- let query = { Id:row.id,taskId: row.taskId,isView:"true" }
+ handleView(row) {
+ let query = {Id: row.id, taskId: row.taskId, isView: "true"}
this.$tab.closeOpenPage({
path: '/part/partAcceptDetail',
query,
@@ -606,25 +689,18 @@ 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);
}
},
@@ -642,24 +718,24 @@ 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: ''
- }
+ }
]
setTimeout(() => {
this.chapter('检验专用章', '安徽送变电工程有限公司机具(物流)分公司')
@@ -669,13 +745,6 @@ export default {
},
//出库检验单打印
printCheck() {
- // this.$refs.remarksPrintRefCheck.print()
- // printJS({
- // printable: 'checkId1',
- // type: 'html',
- // targetStyles: ['*']
- // // 其他配置选项
- // })
this.$nextTick(() => {
printJS({
printable: 'checkId1',
@@ -698,40 +767,32 @@ 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 {
@@ -739,10 +800,10 @@ export default {
}
context.save()
- context.translate(65, 0) // 平移到此位置,此时字和x轴垂直,公司名称和最外圈的距离
- context.rotate(Math.PI / 2) // 旋转90度,让字平行于x轴
- context.strokeStyle = '#ff5050' // 设置印章单位字体颜色为较浅的红色
- context.strokeText(c, 0, 0) // 此点为字的中心点
+ context.translate(65, 0)
+ context.rotate(Math.PI / 2)
+ context.strokeStyle = '#ff5050'
+ context.strokeText(c, 0, 0)
context.restore()
}
},
@@ -750,14 +811,11 @@ 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() //创建路径
- // let x = Math.sin(0);
- // let y = Math.cos(0);
+ context.translate(sx, sy)
+ context.rotate(Math.PI + rotato)
+ context.beginPath()
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)
@@ -769,16 +827,14 @@ 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',
@@ -792,9 +848,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, {
@@ -810,7 +866,6 @@ 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('')
@@ -827,7 +882,6 @@ export default {
}
lines.push(line)
- // 动态计算画布高度
const lineHeight = fontSize + 10
const qrCanvas = document.createElement('canvas')
qrCanvas.width = qrSize + padding * 2
@@ -839,7 +893,6 @@ export default {
ctx.drawImage(img, padding, padding, qrSize, qrSize)
- // 绘制文字
ctx.fillStyle = '#000'
ctx.font = `${fontSize}px Arial`
ctx.textAlign = 'center'
@@ -847,7 +900,6 @@ 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'
@@ -858,7 +910,6 @@ export default {
},
/** 单条下载 */
handleDownload(row) {
- // 构建要传给后端的完整数据结构
const payload = {
items: [
{
@@ -871,43 +922,53 @@ 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.downloadZip(
- '/material/bm_report/downloadSingle',
- JSON.stringify(payload),
- `${row.proName || '文件档案下载'}.zip`
- );
+ this.downloadSingleFile(payload, `${row.proName || '文件档案下载'}.zip`);
},
-
- /**
- * 一键下载(多选)
- */
+ /** 一键下载(多选) */
async handleBulkDownload() {
- const grouped = {};
- try {
- // 检查是否有选中项
- if (!this.selectedItems || this.selectedItems.size === 0) {
- this.$message.warning('请先勾选要下载的行');
- return;
- }
+ // 检查是否有选中项
+ if (this.selectedIds.length === 0) {
+ this.$message.warning('请先勾选要下载的行');
+ return;
+ }
- // 从全局选中状态构建 items,不再只依赖当前页的 tableList
- const items = Array.from(this.selectedItems.values()).map(item => ({
+ console.log('开始下载,选中数量:', this.selectedIds.length);
+
+ // 确认对话框
+ try {
+ await this.$confirm(`确定要下载 ${this.selectedIds.length} 个选中项吗?`, '提示', {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: 'warning',
+ center: true
+ });
+ } catch (cancel) {
+ console.log('用户取消下载');
+ return;
+ }
+
+ // 初始化下载状态
+ this.initializeDownload();
+
+ try {
+ // 构建下载数据 - 直接从selectedData获取
+ const items = Object.values(this.selectedData).map(item => ({
proName: item.proName || '',
departName: item.departName || '',
typeName: item.typeName || '',
@@ -929,54 +990,250 @@ export default {
remark: ''
}));
- // 按工程 -> 领用日期 -> 类型-规格分组
- 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 });
- });
- });
- });
- });
+ console.log('构建了', items.length, '个下载项');
+ // 计算总文件数
+ this.totalItems = this.calculateTotalFiles(items);
+ console.log('总文件数:', this.totalItems);
+ const taskId = crypto.randomUUID()
const payload = {
- items: flatItems,
- zipName: `报告下载_${(new Date()).toISOString().slice(0,10)}`,
+ items: items,
+ taskId:taskId,
+ zipName: `报告下载_${new Date().getTime()}`,
+ stream: true
};
- this.downloadZip(
- '/material/bm_report/downloadBulk',
- JSON.stringify(payload),
- payload.zipName + '.zip'
- );
+ // 开始下载
+ await this.streamDownload(payload);
} catch (err) {
console.error('一键下载失败', err);
- this.$message.error('一键下载失败');
+ this.handleDownloadError(err);
}
- }
+ },
+
+ /** 初始化下载状态 */
+ 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方式 */
+ async streamDownload(payload) {
+ try {
+ this.progressText = '正在连接服务器...';
+
+ // 使用原始的request方法
+ const response = await request({
+ url: '/material/bm_report/downloadBulkStream',
+ 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.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;
+ }
+ },
}
};
+