import React, { useEffect, useMemo, useRef, useState } from "react";
|
import { useTranslate } from "react-admin";
|
import {
|
Box,
|
Button,
|
CircularProgress,
|
Dialog,
|
DialogActions,
|
DialogContent,
|
DialogTitle,
|
ToggleButton,
|
ToggleButtonGroup,
|
Typography,
|
} from "@mui/material";
|
import PrintOutlinedIcon from "@mui/icons-material/PrintOutlined";
|
import { useReactToPrint } from "react-to-print";
|
import DialogCloseButton from "@/page/components/DialogCloseButton";
|
import { getListReportCellValue } from "./listReportUtils";
|
|
const PREVIEW_LAYOUT = {
|
landscape: {
|
width: "297mm",
|
height: "210mm",
|
},
|
portrait: {
|
width: "210mm",
|
height: "297mm",
|
},
|
};
|
|
const PRINT_PAGE_MARGIN = "12mm";
|
|
const buildPrintPageStyle = orientation => `
|
@page {
|
size: A4 ${orientation};
|
margin: 0;
|
}
|
@media print {
|
html, body {
|
width: auto;
|
min-height: auto;
|
height: auto;
|
margin: 0 !important;
|
padding: 0 !important;
|
-webkit-print-color-adjust: exact;
|
print-color-adjust: exact;
|
background: #ffffff;
|
}
|
table {
|
width: 100%;
|
border-collapse: collapse;
|
}
|
thead {
|
display: table-header-group;
|
}
|
.list-report-print-shell {
|
display: block !important;
|
padding: 0 !important;
|
width: 100% !important;
|
max-width: none !important;
|
margin: 0 auto !important;
|
}
|
.list-report-print-live-root {
|
position: static !important;
|
left: auto !important;
|
top: auto !important;
|
visibility: visible !important;
|
background: #ffffff !important;
|
}
|
.list-report-print-page {
|
box-shadow: none !important;
|
border: none !important;
|
margin: 0 auto !important;
|
break-after: page;
|
page-break-after: always;
|
overflow: hidden !important;
|
box-sizing: border-box !important;
|
}
|
.list-report-print-page:last-child {
|
break-after: auto;
|
page-break-after: auto;
|
}
|
.list-report-print-header {
|
page-break-inside: avoid !important;
|
break-inside: avoid !important;
|
}
|
.list-report-print-table-wrap {
|
overflow: visible !important;
|
}
|
.list-report-print-table {
|
width: 100% !important;
|
table-layout: fixed !important;
|
}
|
.list-report-print-table thead {
|
display: table-header-group !important;
|
}
|
.list-report-print-table tr {
|
page-break-inside: avoid !important;
|
break-inside: avoid !important;
|
}
|
}
|
`;
|
|
const getPageBoxSx = (paperSize, printable = false) => ({
|
display: "flex",
|
flexDirection: "column",
|
width: paperSize.width,
|
height: paperSize.height,
|
maxWidth: "100%",
|
mx: "auto",
|
p: PRINT_PAGE_MARGIN,
|
backgroundColor: "#fff",
|
boxShadow: printable ? "none" : "0 8px 24px rgba(15, 23, 42, 0.08)",
|
border: printable ? "none" : "1px solid #d7dce2",
|
boxSizing: "border-box",
|
overflow: "hidden",
|
});
|
|
const ReportHeader = ({ printedAt, reportMeta, totalCount }) => (
|
<>
|
<Box className="list-report-print-header" sx={{ borderBottom: "2px solid #111", pb: 1.25 }}>
|
<Typography
|
variant="h5"
|
sx={{
|
textAlign: "center",
|
fontWeight: 700,
|
letterSpacing: ".02em",
|
color: "#111827",
|
}}
|
>
|
{reportMeta?.title || "报表"}
|
</Typography>
|
</Box>
|
|
<Box
|
className="list-report-print-header"
|
sx={{
|
display: "flex",
|
justifyContent: "space-between",
|
gap: 2,
|
flexWrap: "wrap",
|
borderBottom: "1px solid #111",
|
pb: 1,
|
color: "#111827",
|
fontSize: 14,
|
}}
|
>
|
<Typography variant="body2">报表日期:{reportMeta?.reportDate || printedAt}</Typography>
|
<Typography variant="body2">打印人:{reportMeta?.printedBy || "-"}</Typography>
|
<Typography variant="body2">打印时间:{printedAt}</Typography>
|
<Typography variant="body2">记录数:{totalCount}</Typography>
|
</Box>
|
</>
|
);
|
|
const ReportTable = ({ displayColumns, pageRows, renderRow, tableWrapRef, theadRef, tbodyRef }) => (
|
<Box ref={tableWrapRef} className="list-report-print-table-wrap" sx={{ overflow: "hidden", flex: 1 }}>
|
<table
|
className="list-report-print-table"
|
style={{
|
width: "100%",
|
borderCollapse: "collapse",
|
tableLayout: "fixed",
|
fontSize: "12px",
|
}}
|
>
|
<thead ref={theadRef}>
|
<tr>
|
{displayColumns.map(column => (
|
<th
|
key={column.key}
|
style={{
|
padding: "8px 8px",
|
backgroundColor: "#f1f3f5",
|
textAlign: "center",
|
whiteSpace: "normal",
|
fontWeight: 700,
|
color: "#111827",
|
}}
|
>
|
{column.label}
|
</th>
|
))}
|
</tr>
|
</thead>
|
<tbody ref={tbodyRef}>
|
{pageRows.map(renderRow)}
|
</tbody>
|
</table>
|
</Box>
|
);
|
|
const ReportPageFrame = ({ children, paperSize, printable = false }) => (
|
<Box className="list-report-print-page" sx={getPageBoxSx(paperSize, printable)}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, width: "100%", height: "100%" }}>
|
{children}
|
</Box>
|
</Box>
|
);
|
|
const ListReportPreviewDialog = ({
|
open,
|
onClose,
|
rows = [],
|
columns = [],
|
reportMeta,
|
totalRows = 0,
|
loading = false,
|
allRowsLoaded = false,
|
loadedTransportPages = 0,
|
totalTransportPages = 0,
|
prefetching = false,
|
dialogTitle = "打印预览",
|
defaultOrientation = "landscape",
|
getCellValue = getListReportCellValue,
|
}) => {
|
const translate = useTranslate();
|
const printContentRef = useRef(null);
|
const measurePageRef = useRef(null);
|
const measureContentRef = useRef(null);
|
const measureTableWrapRef = useRef(null);
|
const measureTheadRef = useRef(null);
|
const measureBodyRef = useRef(null);
|
const measureFooterRef = useRef(null);
|
const [orientation, setOrientation] = useState(defaultOrientation);
|
const [currentPage, setCurrentPage] = useState(1);
|
const [isPreparingPrint, setIsPreparingPrint] = useState(false);
|
const [measuring, setMeasuring] = useState(false);
|
const [paginatedRows, setPaginatedRows] = useState([]);
|
|
const printedAt = useMemo(
|
() =>
|
new Date().toLocaleString("zh-CN", {
|
hour12: false,
|
}),
|
[open]
|
);
|
|
const previewPaperSize = useMemo(
|
() => PREVIEW_LAYOUT[orientation] || PREVIEW_LAYOUT.landscape,
|
[orientation]
|
);
|
|
const displayColumns = useMemo(
|
() => [{ key: "__serialNo", label: "序号", source: "__serialNo" }, ...columns],
|
[columns]
|
);
|
|
const totalCount = totalRows || rows.length;
|
const currentPageRows = paginatedRows[Math.max(currentPage - 1, 0)] || [];
|
|
useEffect(() => {
|
if (!open) {
|
setCurrentPage(1);
|
setPaginatedRows([]);
|
setIsPreparingPrint(false);
|
setOrientation(defaultOrientation);
|
}
|
}, [defaultOrientation, open]);
|
|
useEffect(() => {
|
if (currentPage > paginatedRows.length) {
|
setCurrentPage(Math.max(paginatedRows.length, 1));
|
}
|
}, [currentPage, paginatedRows.length]);
|
|
useEffect(() => {
|
if (!open) {
|
return undefined;
|
}
|
if (rows.length === 0 || columns.length === 0) {
|
setPaginatedRows([]);
|
setMeasuring(false);
|
return undefined;
|
}
|
|
setMeasuring(true);
|
const frameId = requestAnimationFrame(() => {
|
const pageElement = measurePageRef.current;
|
const contentElement = measureContentRef.current;
|
const tableWrapElement = measureTableWrapRef.current;
|
const tableHeadElement = measureTheadRef.current;
|
const tableBodyElement = measureBodyRef.current;
|
const footerElement = measureFooterRef.current;
|
|
if (!pageElement || !contentElement || !tableWrapElement || !tableHeadElement || !tableBodyElement || !footerElement) {
|
setPaginatedRows([rows]);
|
setMeasuring(false);
|
return;
|
}
|
|
const availableBodyHeight = Math.max(
|
contentElement.clientHeight -
|
tableWrapElement.offsetTop -
|
tableHeadElement.offsetHeight -
|
footerElement.offsetHeight,
|
80
|
);
|
|
const rowHeights = Array.from(tableBodyElement.querySelectorAll("tr")).map(
|
row => row.getBoundingClientRect().height || row.offsetHeight || 24
|
);
|
|
const nextPages = [];
|
let pageRows = [];
|
let usedHeight = 0;
|
|
rows.forEach((record, index) => {
|
const rowHeight = rowHeights[index] || 24;
|
const exceedsCurrentPage =
|
pageRows.length > 0 && usedHeight + rowHeight > availableBodyHeight;
|
|
if (exceedsCurrentPage) {
|
nextPages.push(pageRows);
|
pageRows = [record];
|
usedHeight = rowHeight;
|
return;
|
}
|
|
pageRows.push(record);
|
usedHeight += rowHeight;
|
});
|
|
if (pageRows.length > 0) {
|
nextPages.push(pageRows);
|
}
|
|
setPaginatedRows(nextPages);
|
setMeasuring(false);
|
});
|
|
return () => cancelAnimationFrame(frameId);
|
}, [columns.length, defaultOrientation, open, orientation, reportMeta?.printedBy, reportMeta?.reportDate, reportMeta?.title, rows]);
|
|
const handlePrint = useReactToPrint({
|
content: () => printContentRef.current,
|
documentTitle: reportMeta?.title || dialogTitle,
|
pageStyle: buildPrintPageStyle(orientation),
|
onAfterPrint: () => {
|
setIsPreparingPrint(false);
|
},
|
});
|
|
useEffect(() => {
|
if (!isPreparingPrint || paginatedRows.length === 0) {
|
return undefined;
|
}
|
|
const firstFrame = requestAnimationFrame(() => {
|
const secondFrame = requestAnimationFrame(() => {
|
handlePrint();
|
});
|
return () => cancelAnimationFrame(secondFrame);
|
});
|
|
return () => cancelAnimationFrame(firstFrame);
|
}, [handlePrint, isPreparingPrint, paginatedRows]);
|
|
const renderDataRow = (record, rowIndex, serialStart, keyPrefix) => (
|
<tr key={record.id ?? `${keyPrefix}-row-${rowIndex}`}>
|
{displayColumns.map(column => (
|
<td
|
key={`${record.id ?? keyPrefix}-${column.key}-${rowIndex}`}
|
style={{
|
padding: "6px 8px",
|
verticalAlign: "top",
|
whiteSpace: "normal",
|
wordBreak: "break-word",
|
textAlign: column.key === "__serialNo" ? "center" : "left",
|
color: "#111827",
|
}}
|
>
|
{column.key === "__serialNo"
|
? String(serialStart + rowIndex + 1).padStart(3, "0")
|
: String(getCellValue(record, column.source))}
|
</td>
|
))}
|
</tr>
|
);
|
|
const renderPage = (pageRows, pageIndex, pageCount, printable = false) => {
|
const serialStart = paginatedRows.slice(0, pageIndex).reduce((sum, page) => sum + page.length, 0);
|
return (
|
<ReportPageFrame key={`preview-page-${pageIndex + 1}`} paperSize={previewPaperSize} printable={printable}>
|
<ReportHeader printedAt={printedAt} reportMeta={reportMeta} totalCount={totalCount} />
|
<ReportTable
|
displayColumns={displayColumns}
|
pageRows={pageRows}
|
renderRow={(record, rowIndex) =>
|
renderDataRow(record, rowIndex, serialStart, `page-${pageIndex + 1}`)
|
}
|
/>
|
<Box
|
sx={{
|
display: "flex",
|
justifyContent: "flex-end",
|
color: "#6b7280",
|
fontSize: 12,
|
}}
|
>
|
第 {pageIndex + 1} / {pageCount} 页
|
</Box>
|
</ReportPageFrame>
|
);
|
};
|
|
const printDisabled =
|
loading ||
|
prefetching ||
|
measuring ||
|
isPreparingPrint ||
|
!allRowsLoaded ||
|
paginatedRows.length === 0 ||
|
columns.length === 0;
|
|
const printButtonLabel = (() => {
|
if (isPreparingPrint) {
|
return "准备打印中...";
|
}
|
if (prefetching || !allRowsLoaded) {
|
return `加载打印数据中... ${loadedTransportPages}/${Math.max(totalTransportPages, 1)}`;
|
}
|
if (measuring) {
|
return "分页计算中...";
|
}
|
return translate("toolbar.print");
|
})();
|
|
return (
|
<Dialog open={open} onClose={onClose} fullWidth maxWidth="xl">
|
<DialogTitle
|
sx={{
|
position: "sticky",
|
top: 0,
|
backgroundColor: "background.paper",
|
zIndex: 1,
|
textAlign: "center",
|
}}
|
>
|
{dialogTitle}
|
<Box sx={{ position: "absolute", right: 8, top: 8 }}>
|
<DialogCloseButton onClose={onClose} />
|
</Box>
|
</DialogTitle>
|
<DialogContent dividers>
|
{loading ? (
|
<Box
|
sx={{
|
minHeight: 280,
|
display: "flex",
|
alignItems: "center",
|
justifyContent: "center",
|
}}
|
>
|
<CircularProgress size={28} />
|
</Box>
|
) : (
|
<Box
|
className="list-report-print-shell"
|
sx={{
|
display: "flex",
|
flexDirection: "column",
|
gap: 2,
|
alignItems: "center",
|
py: 3,
|
}}
|
>
|
{paginatedRows.length > 0
|
? renderPage(
|
currentPageRows,
|
Math.max(currentPage - 1, 0),
|
Math.max(paginatedRows.length, 1)
|
)
|
: (
|
<Box
|
sx={{
|
minHeight: 280,
|
display: "flex",
|
alignItems: "center",
|
justifyContent: "center",
|
color: "#6b7280",
|
}}
|
>
|
{prefetching || measuring ? "正在生成预览..." : "暂无可打印数据"}
|
</Box>
|
)}
|
</Box>
|
)}
|
</DialogContent>
|
<DialogActions sx={{ px: 3, py: 2, justifyContent: "space-between", gap: 2, flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
|
<ToggleButtonGroup
|
value={orientation}
|
exclusive
|
size="small"
|
onChange={(_, value) => {
|
if (value) {
|
setCurrentPage(1);
|
setOrientation(value);
|
}
|
}}
|
>
|
<ToggleButton value="landscape">横版</ToggleButton>
|
<ToggleButton value="portrait">竖版</ToggleButton>
|
</ToggleButtonGroup>
|
<Button
|
size="small"
|
onClick={() => setCurrentPage(page => Math.max(1, page - 1))}
|
disabled={loading || currentPage <= 1}
|
>
|
上一页
|
</Button>
|
<Typography variant="body2" sx={{ minWidth: 104, textAlign: "center" }}>
|
第 {paginatedRows.length === 0 ? 0 : currentPage} / {paginatedRows.length} 页
|
</Typography>
|
<Button
|
size="small"
|
onClick={() => setCurrentPage(page => Math.min(paginatedRows.length, page + 1))}
|
disabled={loading || paginatedRows.length === 0 || currentPage >= paginatedRows.length}
|
>
|
下一页
|
</Button>
|
{(prefetching || measuring) && (
|
<Typography variant="body2" sx={{ color: "#6b7280" }}>
|
{prefetching
|
? `正在加载完整打印数据 ${loadedTransportPages}/${Math.max(totalTransportPages, 1)}`
|
: "正在按纸张尺寸重新分页"}
|
</Typography>
|
)}
|
</Box>
|
<Button
|
variant="contained"
|
startIcon={
|
isPreparingPrint || prefetching || measuring
|
? <CircularProgress size={16} color="inherit" />
|
: <PrintOutlinedIcon />
|
}
|
onClick={() => setIsPreparingPrint(true)}
|
disabled={printDisabled}
|
>
|
{printButtonLabel}
|
</Button>
|
</DialogActions>
|
|
{open && rows.length > 0 && columns.length > 0 && (
|
<Box
|
sx={{
|
position: "fixed",
|
left: "-20000px",
|
top: 0,
|
visibility: "hidden",
|
pointerEvents: "none",
|
backgroundColor: "#fff",
|
}}
|
>
|
<ReportPageFrame paperSize={previewPaperSize} printable>
|
<Box ref={measurePageRef} sx={{ width: "100%", height: "100%" }}>
|
<Box ref={measureContentRef} sx={{ display: "flex", flexDirection: "column", gap: 2, width: "100%", height: "100%" }}>
|
<ReportHeader printedAt={printedAt} reportMeta={reportMeta} totalCount={totalCount} />
|
<ReportTable
|
displayColumns={displayColumns}
|
pageRows={rows}
|
renderRow={(record, rowIndex) => renderDataRow(record, rowIndex, 0, "measure")}
|
tableWrapRef={measureTableWrapRef}
|
theadRef={measureTheadRef}
|
tbodyRef={measureBodyRef}
|
/>
|
<Box
|
ref={measureFooterRef}
|
sx={{
|
display: "flex",
|
justifyContent: "flex-end",
|
color: "#6b7280",
|
fontSize: 12,
|
}}
|
>
|
第 1 / 1 页
|
</Box>
|
</Box>
|
</Box>
|
</ReportPageFrame>
|
</Box>
|
)}
|
|
{isPreparingPrint && (
|
<Box
|
ref={printContentRef}
|
className="list-report-print-shell list-report-print-live-root"
|
sx={{
|
position: "fixed",
|
left: "-10000px",
|
top: 0,
|
backgroundColor: "#fff",
|
px: 3,
|
py: 3,
|
}}
|
>
|
{paginatedRows.map((pageRows, pageIndex) =>
|
renderPage(pageRows, pageIndex, paginatedRows.length, true)
|
)}
|
</Box>
|
)}
|
</Dialog>
|
);
|
};
|
|
export default ListReportPreviewDialog;
|