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;