import React, { useMemo, useRef, useState } from "react";
|
import DownloadIcon from "@mui/icons-material/GetApp";
|
import PrintOutlinedIcon from "@mui/icons-material/PrintOutlined";
|
import SaveIcon from '@mui/icons-material/Save';
|
import {
|
Box,
|
Button as MuiButton,
|
CircularProgress,
|
Dialog,
|
DialogActions,
|
DialogContent,
|
DialogTitle,
|
Paper,
|
Table,
|
TableBody,
|
TableCell,
|
TableContainer,
|
TableHead,
|
TableRow,
|
Typography,
|
} from '@mui/material';
|
import {
|
Button,
|
useDataProvider,
|
useListContext,
|
useNotify,
|
useStore,
|
useTranslate,
|
useUnselectAll,
|
} from "react-admin";
|
import { useReactToPrint } from "react-to-print";
|
import DialogCloseButton from "./DialogCloseButton";
|
import { getValueBySource, resolveVisibleColumns } from "./listExportPrintUtils";
|
|
const PREVIEW_ROW_LIMIT = 200;
|
|
const formatColumnLabel = (label, translate) => {
|
if (!label) {
|
return '';
|
}
|
|
if (typeof label !== 'string') {
|
return String(label);
|
}
|
|
if (label.includes('.')) {
|
return translate(label, { _: label });
|
}
|
|
return label;
|
};
|
|
const formatValue = (value, fieldType) => {
|
if (value == null) {
|
return '';
|
}
|
|
if (fieldType === 'boolean') {
|
return value ? 'true' : 'false';
|
}
|
|
return String(value);
|
};
|
|
const padNumber = (value) => String(value).padStart(2, '0');
|
|
const formatDate = (date = new Date()) => (
|
`${date.getFullYear()}年${padNumber(date.getMonth() + 1)}月${padNumber(date.getDate())}日`
|
);
|
|
const formatDateTime = (date = new Date()) => (
|
`${date.getFullYear()}/${padNumber(date.getMonth() + 1)}/${padNumber(date.getDate())} ${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}`
|
);
|
|
const getPrintLayoutConfig = (columnCount) => {
|
if (columnCount >= 16) {
|
return {
|
bodyFontSize: 8,
|
headerFontSize: 8,
|
cellPaddingX: 0.35,
|
cellPaddingY: 0.4,
|
sequenceWidth: 36,
|
};
|
}
|
|
if (columnCount >= 12) {
|
return {
|
bodyFontSize: 9,
|
headerFontSize: 9,
|
cellPaddingX: 0.45,
|
cellPaddingY: 0.5,
|
sequenceWidth: 42,
|
};
|
}
|
|
return {
|
bodyFontSize: 10,
|
headerFontSize: 10,
|
cellPaddingX: 0.6,
|
cellPaddingY: 0.65,
|
sequenceWidth: 52,
|
};
|
};
|
|
const getPrintOperator = () => {
|
try {
|
const user = JSON.parse(localStorage.getItem('user'));
|
return user?.fullName || user?.username || 'SYSTEM';
|
} catch (error) {
|
return 'SYSTEM';
|
}
|
};
|
|
const buildReportMeta = (reportTitle, count = 0) => ({
|
reportTitle,
|
reportDate: formatDate(),
|
printedAt: formatDateTime(),
|
operator: getPrintOperator(),
|
count,
|
});
|
|
const getRecordKey = (record, index) => (
|
record?.id ?? record?.barcode ?? record?.code ?? `row-${index}`
|
);
|
|
const PrintReportContent = ({
|
contentRef,
|
rows,
|
reportTitle,
|
printMeta,
|
translatedColumns,
|
printLayoutConfig,
|
sequenceColumnWidth,
|
dataColumnWidth,
|
previewNotice,
|
}) => (
|
<Box
|
ref={contentRef}
|
sx={{
|
width: '277mm',
|
minHeight: '186mm',
|
mx: 'auto',
|
px: 1.5,
|
py: 2,
|
backgroundColor: '#fff',
|
color: '#111827',
|
boxSizing: 'border-box',
|
}}
|
>
|
<Typography
|
variant="h5"
|
sx={{
|
mb: 1,
|
textAlign: 'center',
|
fontWeight: 700,
|
letterSpacing: '0.08em',
|
}}
|
>
|
{reportTitle}
|
</Typography>
|
<Box
|
sx={{
|
mb: 1.5,
|
borderBottom: '2px solid #111827',
|
}}
|
/>
|
<Box
|
sx={{
|
display: 'flex',
|
justifyContent: 'space-between',
|
gap: 2,
|
flexWrap: 'wrap',
|
mb: 2,
|
fontSize: 12,
|
color: '#374151',
|
}}
|
>
|
<Typography variant="body2">报表日期: {printMeta.reportDate}</Typography>
|
<Typography variant="body2">打印人: {printMeta.operator}</Typography>
|
<Typography variant="body2">打印时间: {printMeta.printedAt}</Typography>
|
<Typography variant="body2">记录数: {printMeta.count}</Typography>
|
</Box>
|
{previewNotice && (
|
<Typography variant="body2" sx={{ mb: 1.5, color: '#6b7280' }}>
|
{previewNotice}
|
</Typography>
|
)}
|
<TableContainer
|
component={Paper}
|
sx={{
|
boxShadow: 'none',
|
border: 'none',
|
backgroundColor: 'transparent',
|
}}
|
>
|
<Table
|
size="small"
|
sx={{
|
width: '100%',
|
tableLayout: 'fixed',
|
'& .MuiTableCell-root': {
|
borderBottom: 'none',
|
fontSize: printLayoutConfig.bodyFontSize,
|
px: printLayoutConfig.cellPaddingX,
|
py: printLayoutConfig.cellPaddingY,
|
whiteSpace: 'normal',
|
wordBreak: 'break-all',
|
overflowWrap: 'anywhere',
|
lineHeight: 1.35,
|
verticalAlign: 'top',
|
boxSizing: 'border-box',
|
},
|
'& .MuiTableHead-root .MuiTableCell-root': {
|
backgroundColor: '#f9fafb',
|
fontWeight: 700,
|
fontSize: printLayoutConfig.headerFontSize,
|
textAlign: 'center',
|
},
|
'& .MuiTableRow-root': {
|
pageBreakInside: 'avoid',
|
breakInside: 'avoid',
|
},
|
}}
|
>
|
<colgroup>
|
<col style={{ width: sequenceColumnWidth }} />
|
{translatedColumns.map((column) => (
|
<col key={column.source} style={{ width: dataColumnWidth }} />
|
))}
|
</colgroup>
|
<TableHead>
|
<TableRow>
|
<TableCell align="center">
|
序号
|
</TableCell>
|
{translatedColumns.map((column) => (
|
<TableCell key={column.source}>
|
{column.labelText}
|
</TableCell>
|
))}
|
</TableRow>
|
</TableHead>
|
<TableBody>
|
{rows.map((record, index) => (
|
<TableRow key={getRecordKey(record, index)}>
|
<TableCell align="center">
|
{padNumber(index + 1)}
|
</TableCell>
|
{translatedColumns.map((column) => (
|
<TableCell key={column.source}>
|
{formatValue(getValueBySource(record, column.source), column.fieldType)}
|
</TableCell>
|
))}
|
</TableRow>
|
))}
|
</TableBody>
|
</Table>
|
</TableContainer>
|
</Box>
|
);
|
|
const ListExportPrintButton = ({
|
storeKey,
|
columns = [],
|
title,
|
fileName,
|
maxResults = 1000,
|
}) => {
|
const {
|
filter,
|
selectedIds = [],
|
filterValues = {},
|
resource,
|
sort,
|
total = 0,
|
} = useListContext();
|
const [hiddenColumns] = useStore(
|
storeKey,
|
columns.filter((column) => column.hidden).map((column) => column.source)
|
);
|
const [columnRanks] = useStore(`${storeKey}_columnRanks`, []);
|
const dataProvider = useDataProvider();
|
const notify = useNotify();
|
const translate = useTranslate();
|
const unselectAll = useUnselectAll(resource);
|
const previewContentRef = useRef(null);
|
const fullPrintContentRef = useRef(null);
|
const [printOpen, setPrintOpen] = useState(false);
|
const [printLoading, setPrintLoading] = useState(false);
|
const [printRows, setPrintRows] = useState([]);
|
const [renderFullPrintContent, setRenderFullPrintContent] = useState(false);
|
|
const defaultHiddenColumns = useMemo(
|
() => columns.filter((column) => column.hidden).map((column) => column.source),
|
[columns]
|
);
|
|
const visibleColumns = useMemo(
|
() => resolveVisibleColumns(storeKey, defaultHiddenColumns, columns, hiddenColumns, columnRanks),
|
[storeKey, defaultHiddenColumns, columns, hiddenColumns, columnRanks]
|
);
|
|
const mergedFilter = useMemo(
|
() => (filter ? { ...filterValues, ...filter } : filterValues),
|
[filter, filterValues]
|
);
|
|
const translatedTitle = useMemo(
|
() => formatColumnLabel(title || resource, translate),
|
[title, resource, translate]
|
);
|
|
const reportTitle = useMemo(() => `${translatedTitle}报表`, [translatedTitle]);
|
const [printMeta, setPrintMeta] = useState(() => buildReportMeta(reportTitle, 0));
|
|
const translatedColumns = useMemo(
|
() => visibleColumns.map((column) => ({
|
...column,
|
labelText: formatColumnLabel(column.label, translate),
|
})),
|
[visibleColumns, translate]
|
);
|
const previewRows = useMemo(
|
() => printRows.slice(0, PREVIEW_ROW_LIMIT),
|
[printRows]
|
);
|
const previewNotice = printRows.length > PREVIEW_ROW_LIMIT
|
? `当前仅预览前 ${PREVIEW_ROW_LIMIT} 条,打印将包含全部 ${printRows.length} 条记录`
|
: '';
|
const printLayoutConfig = useMemo(
|
() => getPrintLayoutConfig(translatedColumns.length),
|
[translatedColumns.length]
|
);
|
const sequenceColumnWidth = `${printLayoutConfig.sequenceWidth}px`;
|
const dataColumnWidth = translatedColumns.length > 0
|
? `calc((100% - ${sequenceColumnWidth}) / ${translatedColumns.length})`
|
: 'auto';
|
|
const isDisabled = total === 0 || translatedColumns.length === 0;
|
|
const getPerPage = () => {
|
if (selectedIds.length > 0) {
|
return Math.min(selectedIds.length, maxResults);
|
}
|
|
if (total > 0) {
|
return Math.min(total, maxResults);
|
}
|
|
return maxResults;
|
};
|
|
const downloadBlob = (response) => {
|
const url = window.URL.createObjectURL(
|
new Blob([response.data], { type: response.headers["content-type"] }),
|
);
|
const link = document.createElement("a");
|
link.href = url;
|
link.setAttribute("download", `${fileName || resource}.xlsx`);
|
document.body.appendChild(link);
|
link.click();
|
link.remove();
|
window.URL.revokeObjectURL(url);
|
};
|
|
const handleExport = (event) => {
|
if (translatedColumns.length === 0) {
|
notify('暂无可导出的列', { type: 'warning' });
|
return;
|
}
|
|
dataProvider
|
.export(resource, {
|
sort,
|
ids: selectedIds,
|
filter: mergedFilter,
|
pagination: { page: 1, perPage: getPerPage() },
|
meta: {
|
...buildReportMeta(reportTitle, selectedIds.length > 0 ? selectedIds.length : total),
|
columns: translatedColumns.map((column) => ({
|
source: column.source,
|
label: column.labelText,
|
})),
|
},
|
})
|
.then((response) => {
|
downloadBlob(response);
|
unselectAll();
|
})
|
.catch((error) => {
|
console.error(error);
|
notify("ra.notification.http_error", { type: "error" });
|
});
|
|
if (typeof event?.stopPropagation === 'function') {
|
event.stopPropagation();
|
}
|
};
|
|
const loadPrintRows = async () => {
|
if (translatedColumns.length === 0) {
|
notify('暂无可打印的列', { type: 'warning' });
|
return;
|
}
|
|
setPrintOpen(true);
|
setPrintLoading(true);
|
try {
|
let rows = [];
|
if (selectedIds.length > 0) {
|
const response = await dataProvider.getMany(resource, { ids: selectedIds });
|
rows = response.data || [];
|
} else {
|
const response = await dataProvider.getList(resource, {
|
sort,
|
filter: mergedFilter,
|
pagination: { page: 1, perPage: getPerPage() },
|
});
|
rows = response.data || [];
|
}
|
setPrintRows(rows);
|
setPrintMeta(buildReportMeta(reportTitle, rows.length));
|
} catch (error) {
|
console.error(error);
|
notify("ra.notification.http_error", { type: "error" });
|
setPrintOpen(false);
|
} finally {
|
setPrintLoading(false);
|
}
|
};
|
|
const handlePrintDialogClose = () => {
|
setPrintOpen(false);
|
setPrintRows([]);
|
};
|
|
const triggerPrint = useReactToPrint({
|
content: () => fullPrintContentRef.current,
|
documentTitle: translatedTitle,
|
onAfterPrint: () => {
|
setRenderFullPrintContent(false);
|
},
|
pageStyle: `
|
@page {
|
size: A4 landscape;
|
margin: 12mm 10mm;
|
}
|
html, body {
|
width: 297mm;
|
min-height: 210mm;
|
}
|
body {
|
-webkit-print-color-adjust: exact;
|
print-color-adjust: exact;
|
margin: 0;
|
}
|
`,
|
});
|
|
const handlePrint = async () => {
|
if (printLoading || printRows.length === 0) {
|
return;
|
}
|
|
setRenderFullPrintContent(true);
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
if (typeof triggerPrint === 'function') {
|
triggerPrint();
|
} else {
|
setRenderFullPrintContent(false);
|
}
|
};
|
|
return (
|
<>
|
<Button
|
onClick={handleExport}
|
label="ra.action.export"
|
disabled={isDisabled}
|
>
|
<DownloadIcon />
|
</Button>
|
<Button
|
onClick={loadPrintRows}
|
label="toolbar.print"
|
disabled={isDisabled}
|
>
|
<PrintOutlinedIcon />
|
</Button>
|
<Dialog open={printOpen} maxWidth="xl" fullWidth onClose={handlePrintDialogClose}>
|
<DialogCloseButton onClose={handlePrintDialogClose} />
|
<DialogTitle>{translatedTitle}</DialogTitle>
|
<DialogContent dividers>
|
{printLoading ? (
|
<Box sx={{ py: 8, display: 'flex', justifyContent: 'center' }}>
|
<CircularProgress size={28} />
|
</Box>
|
) : printRows.length === 0 ? (
|
<Box sx={{ py: 8, textAlign: 'center' }}>
|
<Typography color="text.secondary">暂无可打印数据</Typography>
|
</Box>
|
) : (
|
<PrintReportContent
|
contentRef={previewContentRef}
|
rows={previewRows}
|
reportTitle={reportTitle}
|
printMeta={printMeta}
|
translatedColumns={translatedColumns}
|
printLayoutConfig={printLayoutConfig}
|
sequenceColumnWidth={sequenceColumnWidth}
|
dataColumnWidth={dataColumnWidth}
|
previewNotice={previewNotice}
|
/>
|
)}
|
</DialogContent>
|
<DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper' }}>
|
<MuiButton
|
onClick={handlePrint}
|
disabled={printLoading || printRows.length === 0}
|
variant="contained"
|
startIcon={<SaveIcon />}
|
>
|
{translate('toolbar.confirm')}
|
</MuiButton>
|
</DialogActions>
|
</Dialog>
|
{renderFullPrintContent && (
|
<Box sx={{ position: 'fixed', left: '-10000px', top: 0, zIndex: -1 }}>
|
<PrintReportContent
|
contentRef={fullPrintContentRef}
|
rows={printRows}
|
reportTitle={reportTitle}
|
printMeta={printMeta}
|
translatedColumns={translatedColumns}
|
printLayoutConfig={printLayoutConfig}
|
sequenceColumnWidth={sequenceColumnWidth}
|
dataColumnWidth={dataColumnWidth}
|
/>
|
</Box>
|
)}
|
</>
|
);
|
};
|
|
export default ListExportPrintButton;
|