zy-react-library/src/components/Excel/index.js

229 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;