2025新版
接口定义
@PostMapping("/exportWelfareCertificate") @Operation(summary = "导出福利报销凭证") public void exportWelfareCertificate(@RequestBody OasCostSharingCertificateExportVo reqParam, HttpServletResponse response) { List<OasCostSharingCredentialExcelVo> excelDataList = costSharingService.getWelfareCertificateData(reqParam); String url = null; String fileName = "福利费凭证_" + DateUtil.format(new Date(),"YYYYMMdd_HHmmss") + ".xlsx"; String objectKey = "oas/costSharing/welfare/" + fileName; try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); InputStream templateStream = new ByteArrayInputStream( HttpUtil.downloadBytes(CostSharingConstant.certificateTemplateUrl))) { EasyExcel.write(bos).withTemplate(templateStream).sheet().doFill(excelDataList); byte[] bytes = bos.toByteArray();
url = MinioUtil.upload(new ByteArrayInputStream(bytes), objectKey);
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"; " + "filename*=UTF-8''" + encodedFileName); response.setContentLength(bytes.length); try (OutputStream out = response.getOutputStream()) { out.write(bytes); } } catch (Exception e) { log.error("oas:费用分摊,导出福利凭证失败", e); } finally { try { Map<String, String> params = Map.of( "billType", reqParam.getBillType(), "selectedBill", reqParam.getSelectedBill(), "fsDeptInfoId", reqParam.getFsDeptInfoId() ); costSharingLogService.insertLog(CostSharingConstant.MODULE_TYPE.WELFARE, CostSharingConstant.EVENT_TYPE.EXPORT, fileName, url, JSON.toJSONString(params)); } catch (Exception logEx) { log.warn("记录导出日志失败", logEx); } } }
|
api定义与使用
export const exportWelfareCertificateApi = (params: { dataList; totalAmountExcludingTax; totalTax; totalAmount; billDate; billType; selectedBill; fsDeptInfoId; }) => { return defHttp.post( { url: Api.exportWelfareCertificate, params, responseType: 'blob', timeout: 1000000, }, { isTransformResponse: false, isReturnNativeResponse: true } ); };
let response = await exportWelfareCertificateApi({ dataList: allLeafHasPersonNumNodeArr, totalAmountExcludingTax: costSharingItem.value.totalAmountExcludingTax, totalTax: costSharingItem.value.totalTax, totalAmount: costSharingItem.value.totalAmount, billDate: costSharingItem.value.selectedInvoice[0].billDate, billType: billType.value, selectedBill: JSON.stringify( costSharingItem.value.selectedInvoice.map((e) => ({ deptId: e.deptId, deptName: e.deptName, billDate: e.billDate, djbh: e.djbh, pkJkbx: e.pkJkbx, djrq: e.djrq, zy: e.zy, total: e.total, })) ), fsDeptInfoId: costSharingItem.value.fsDeptInfoId, }); const blob = new Blob([response.data], { type: response.data.type || 'application/octet-stream' }); const cd = response.headers?.['content-disposition']; downloadBlobSmart(blob, '福利费凭证.xlsx', cd);
|
工具类
declare global { interface Navigator { msSaveOrOpenBlob?: (blob: Blob, defaultName?: string) => boolean; msSaveBlob?: (blob: Blob, defaultName?: string) => boolean; } }
function parseDispositionFileName(header?: string | null): string | null { if (!header) return null;
const utf8Match = header.match(/filename\*\s*=\s*([^']*)''([^;]+)/i); if (utf8Match && utf8Match[2]) { try { return decodeURIComponent(utf8Match[2]); } catch { } }
const quoted = header.match(/filename\s*=\s*"([^"]+)"/i); if (quoted && quoted[1]) { return quoted[1]; }
const unquoted = header.match(/filename\s*=\s*([^;]+)/i); if (unquoted && unquoted[1]) { return unquoted[1].trim(); }
return null; }
function sanitizeAndTruncateFileName(inputName: string): string { const illegalRe = /[<>:"/\\|?*\x00-\x1F]/g; const windowsTrailingRe = /[. ]+$/; let name = inputName.replace(illegalRe, '_').replace(windowsTrailingRe, '_');
if (name.length > 200) { const lastDot = name.lastIndexOf('.'); if (lastDot > 0 && lastDot >= name.length - 15) { const ext = name.slice(lastDot); const baseMax = 200 - ext.length; name = name.slice(0, Math.max(1, baseMax)) + ext; } else { name = name.slice(0, 200); } } return name || 'download'; }
function isIOSLike(): boolean { const ua = navigator.userAgent; const iOS = /iPad|iPhone|iPod/i.test(ua); const iPadOS = navigator.platform === 'MacIntel' && (navigator as any).maxTouchPoints > 1; return iOS || iPadOS; }
export function downloadBlobSmart(blob: Blob, defaultFileName: string, contentDisposition?: string | null): void { const parsedName = parseDispositionFileName(contentDisposition); const fileName = sanitizeAndTruncateFileName(parsedName || defaultFileName || 'download');
if (typeof navigator.msSaveOrOpenBlob === 'function') { navigator.msSaveOrOpenBlob(blob, fileName); return; } if (typeof navigator.msSaveBlob === 'function') { navigator.msSaveBlob(blob, fileName); return; }
const a = document.createElement('a'); const supportsDownload = 'download' in a; const iOS = isIOSLike();
if (!iOS && supportsDownload) { const url = URL.createObjectURL(blob); a.style.display = 'none'; a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 0); return; }
const reader = new FileReader(); reader.onload = () => { const dataUrl = reader.result as string; const win = window.open(); if (win) { win.document.write('<iframe src="' + dataUrl + '" style="display:none;"></iframe>'); } else { window.location.href = dataUrl; } }; reader.readAsDataURL(blob); }
|
返回整体文件流,通过浏览器转成二进制触发下载
该方式和传统的方式不同。
传统是读取服务器返回的流,这里是服务器流都已经整体返回了,然后才通过 js 转成文件触发下载
该方式只适合下载小文件(一般是小于 10M),如果文件过大,会导致浏览器占用内存过大,页面崩溃
以下载 excel 为例子
前台代码
export function downloadImportTemplate(url, fileName) { return request({ type: 'get', url, responseType: 'blob' }).then((res) => { const url = window.URL.createObjectURL(new Blob([res])) const link = document.createElement('a') link.style.display = 'none' link.href = url link.setAttribute('download', fileName + '.xlsx') document.body.appendChild(link) link.click() }) }
downloadImportTemplate('/downloadTemplate', '工程车辆导入模板')
|
@RequestMapping("/downloadTemplate") public void downLoadEngineeringVehiclesImportTemplateFile(HttpServletResponse response) { downLoadTemplate("工程车导入模板", response); }
public void downLoadTemplate(String name, HttpServletResponse response) { String path = "template/" + name + ".xlsx"; byte[] buffer = new byte[1024]; int len = 0; try (InputStream in = this.getClass().getClassLoader().getResourceAsStream(path); OutputStream out = response.getOutputStream()) { response.setHeader( "Content-disposition", "attachment;filename=" + new String((name + ".xlsx").getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
while ((len = in.read(buffer)) > 0) { out.write(buffer, 0, len); } } catch (Exception e) { e.printStackTrace(); } }
|