import JSZip from 'jszip'; // 通用表格导出:CSV(带 BOM,Excel 直接打开中文不乱码)与 XLSX(基于 jszip 生成最小 OOXML 工作簿)。 // columns 复用 antd Table 列定义(title/dataIndex),可选 exportValue(value, row) 提供导出值格式化(与 antd render 的 value 语义一致)。 export const escapeXmlText = (value) => String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); export const spreadsheetColumnName = (index) => { let name = ''; let value = index + 1; while (value > 0) { const remainder = (value - 1) % 26; name = String.fromCharCode(65 + remainder) + name; value = Math.floor((value - 1) / 26); } return name; }; const resolveCell = (column, row) => { const value = row?.[column?.dataIndex]; return typeof column?.exportValue === 'function' ? column.exportValue(value, row) : value; }; export const resolveExportRows = (columns, rows) => ( (rows || []).map((row) => columns.map((column) => resolveCell(column, row))) ); const saveBlob = (blob, fileName) => { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); }; // 服务端直接返回文件的场景(/export 接口,responseType: 'blob')在这里统一落盘 export const downloadBlobAsFile = (blob, fileName) => saveBlob(blob, fileName); // 导出接口出错时,错误响应体同样是 Blob(内含 JSON),这里统一解出可读文案 export const readBlobErrorMessage = async (error, fallback = '导出失败') => { const data = error?.response?.data; if (typeof Blob !== 'undefined' && data instanceof Blob) { try { const text = await data.text(); try { const parsed = JSON.parse(text); return parsed?.error || parsed?.message || fallback; } catch { return text.slice(0, 300) || fallback; } } catch { return fallback; } } return error?.response?.data?.error || error?.message || fallback; }; const quoteCsvCell = (value) => { const text = value === null || value === undefined ? '' : String(value); return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; }; export const buildCsvContent = (columns, rows) => { const header = columns.map((column) => quoteCsvCell(column?.title ?? '')).join(','); const body = resolveExportRows(columns, rows) .map((cells) => cells.map((cell) => quoteCsvCell(cell)).join(',')) .join('\r\n'); return body ? `${header}\r\n${body}` : header; }; export const exportTableToCsv = (fileName, columns, rows) => { const content = buildCsvContent(columns, rows); saveBlob(new Blob([`\ufeff${content}`], { type: 'text/csv;charset=utf-8;' }), `${fileName}.csv`); }; const CONTENT_TYPES_XML = '' + '' + '' + '' + '' + '' + ''; const ROOT_RELS_XML = '' + '' + '' + ''; const WORKBOOK_RELS_XML = '' + '' + '' + ''; const workbookXml = (sheetName) => '' + '' + `` + ''; const cellXml = (value, rowIndex, columnIndex) => { const ref = `${spreadsheetColumnName(columnIndex)}${rowIndex + 1}`; if (value === null || value === undefined || value === '') { return ``; } if (typeof value === 'number' && Number.isFinite(value)) { return `${value}`; } return `${escapeXmlText(value)}`; }; export const buildWorksheetXml = (columns, rows) => { const lines = [columns.map((column, columnIndex) => cellXml(column?.title ?? '', 0, columnIndex)).join('')]; resolveExportRows(columns, rows).forEach((cells, rowIndex) => { lines.push(cells.map((cell, columnIndex) => cellXml(cell, rowIndex + 1, columnIndex)).join('')); }); return '' + '' + `${lines.map((row) => `${row}`).join('')}` + ''; }; export const exportTableToXlsx = async (fileName, columns, rows, sheetName = '数据') => { const zip = new JSZip(); zip.file('[Content_Types].xml', CONTENT_TYPES_XML); zip.file('_rels/.rels', ROOT_RELS_XML); zip.file('xl/workbook.xml', workbookXml(sheetName)); zip.file('xl/_rels/workbook.xml.rels', WORKBOOK_RELS_XML); zip.file('xl/worksheets/sheet1.xml', buildWorksheetXml(columns, rows)); const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }); saveBlob(blob, `${fileName}.xlsx`); }; export const exportTableFile = async (format, fileName, columns, rows, sheetName) => { if (format === 'xlsx') { await exportTableToXlsx(fileName, columns, rows, sheetName); return; } exportTableToCsv(fileName, columns, rows); };