import { Button, message, Modal, Spin } from "antd"; import ExcelJS from "exceljs"; import { useEffect, useRef, useState } from "react"; import Spreadsheet from "x-data-spreadsheet"; import useDownloadFile from "../../hooks/useDownloadFile"; import { getFileUrl } from "../../utils"; import "x-data-spreadsheet/dist/xspreadsheet.css"; /** * Excel 查看组件。 * 使用 exceljs 解析 xlsx,再通过 x-data-spreadsheet 渲染表格预览。 */ function Excel(props) { const { visible = false, onCancel, file, name, inline = false, title = "Excel预览", style = {}, extraButtons, } = props; const containerRef = useRef(null); const fullscreenRef = useRef(null); const unmountedRef = useRef(false); const fileUrl = getFileUrl(); const [loading, setLoading] = useState(false); const { downloadFile } = useDownloadFile(); const renderExcel = async () => { setLoading(true); containerRef.current.innerHTML = ""; try { const response = await fetch(!file.includes(fileUrl) ? fileUrl + file : file); if (!response.ok) throw new Error("加载 Excel 文件失败"); const data = await response.arrayBuffer(); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(data); if (unmountedRef.current) return; new Spreadsheet(containerRef.current, { mode: "read", showToolbar: false, showContextmenu: false, view: { height: () => containerRef.current?.clientHeight || 520, width: () => containerRef.current?.clientWidth || 800, }, }).loadData(convertWorkbookToSpreadsheet(workbook)); } catch { message.error("加载 Excel 文件失败"); if (!inline && onCancel) onCancel(); } finally { if (!unmountedRef.current) setLoading(false); } }; useEffect(() => { if ((!visible && !inline) || !file) return; unmountedRef.current = false; const initTimer = setTimeout(() => { renderExcel(); }, 100); return () => { unmountedRef.current = true; if (containerRef.current) containerRef.current.innerHTML = ""; if (initTimer) clearTimeout(initTimer); }; }, [visible, inline, file]); const renderExcelContent = () => (
); if (inline) return renderExcelContent(); const onDownloadFile = () => { downloadFile({ url: file, name, }); }; return (
{ onCancel(); }} getContainer={false} footer={[ , , ...(extraButtons || []), ]} > {renderExcelContent()}
); } function convertWorkbookToSpreadsheet(workbook) { const sheets = []; workbook.worksheets.forEach((worksheet) => { sheets.push(convertWorksheetToSheetData(worksheet)); }); return sheets.length > 0 ? sheets : [{ name: "Sheet1", rows: {} }]; } function convertWorksheetToSheetData(worksheet) { const sheetData = { name: worksheet.name, rows: {}, cols: { len: worksheet.columnCount || 26 }, merges: worksheet.model?.merges || [], }; worksheet.columns.forEach((column, index) => { if (column.width) sheetData.cols[index] = { width: Math.round(column.width * 8) }; }); // x-data-spreadsheet 使用 0 起始下标,exceljs 使用 1 起始下标,这里统一转换。 worksheet.eachRow({ includeEmpty: true }, (row, rowIndex) => { const rowData = { cells: {} }; if (row.height) rowData.height = Math.round(row.height * 1.4); row.eachCell({ includeEmpty: true }, (cell, colIndex) => { rowData.cells[colIndex - 1] = { text: getCellText(cell), }; }); sheetData.rows[rowIndex - 1] = rowData; }); (sheetData.merges || []).forEach((merge) => { const range = parseExcelRange(merge); const cell = sheetData.rows[range.sri]?.cells?.[range.sci]; if (cell) cell.merge = [range.eri - range.sri, range.eci - range.sci]; }); return sheetData; } function getCellText(cell) { if (cell.value === null || cell.value === undefined) return ""; if (cell.formula) return cell.result === null || cell.result === undefined ? `=${cell.formula}` : String(cell.result); if (cell.text) return cell.text; if (cell.value instanceof Date) return cell.value.toLocaleDateString(); if (typeof cell.value === "object") { if (cell.value.text) return cell.value.text; if (cell.value.richText) return cell.value.richText.map(item => item.text).join(""); if (cell.value.result !== undefined) return String(cell.value.result); } return String(cell.value); } function parseExcelRange(range) { const [start, end = start] = range.split(":"); const startCell = parseExcelCell(start); const endCell = parseExcelCell(end); return { sri: startCell.rowIndex, sci: startCell.colIndex, eri: endCell.rowIndex, eci: endCell.colIndex, }; } function parseExcelCell(cell) { const match = /^([A-Z]+)(\d+)$/i.exec(cell); if (!match) return { rowIndex: 0, colIndex: 0 }; return { rowIndex: Number(match[2]) - 1, colIndex: getColumnIndex(match[1]), }; } function getColumnIndex(columnName) { return columnName.toUpperCase().split("").reduce((total, char) => { return total * 26 + char.charCodeAt(0) - 64; }, 0) - 1; } Excel.displayName = "Excel"; export default Excel;