227 lines
6.0 KiB
JavaScript
227 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 = {},
|
|||
|
|
} = 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>,
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{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;
|