229 lines
6.0 KiB
JavaScript
229 lines
6.0 KiB
JavaScript
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 = () => (
|
||
<Spin spinning={loading}>
|
||
<div
|
||
style={{ height: "72vh", overflow: "hidden", ...style }}
|
||
>
|
||
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
|
||
</div>
|
||
</Spin>
|
||
);
|
||
|
||
if (inline)
|
||
return renderExcelContent();
|
||
|
||
const onDownloadFile = () => {
|
||
downloadFile({
|
||
url: file,
|
||
name,
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div ref={fullscreenRef}>
|
||
<Modal
|
||
style={{ top: 100, maxWidth: "calc(100vw - 32px)", paddingBottom: 24 }}
|
||
open={visible}
|
||
mask={{ closable: false }}
|
||
width={1000}
|
||
title={title}
|
||
onCancel={() => {
|
||
onCancel();
|
||
}}
|
||
getContainer={false}
|
||
footer={[
|
||
<Button
|
||
key="cancel"
|
||
onClick={() => {
|
||
onCancel();
|
||
}}
|
||
>
|
||
关闭
|
||
</Button>,
|
||
<Button key="download" type="primary" onClick={onDownloadFile}>下载</Button>,
|
||
...(extraButtons || []),
|
||
]}
|
||
>
|
||
{renderExcelContent()}
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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;
|