From 6e5ff559023efd2d24fdca2adcb7268d06420e46 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期二, 24 三月 2026 15:38:34 +0800
Subject: [PATCH] #打印+导出

---
 rsf-admin/src/page/components/MyExportButton.jsx                                                        |   87 +
 rsf-admin/src/page/components/listReport/ListReportPreviewDialog.jsx                                    |  607 +++++++++++
 rsf-admin/src/page/components/listReport/useListReportOutput.js                                         |  292 +++++
 rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemPrintPreview.jsx                                |    1 
 rsf-admin/src/page/warehouseAreasItem/warehouseAreasItemOutputUtils.jsx                                 |  493 +++++++++
 rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WarehouseAreasItemServiceImpl.java |   35 
 rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemList.jsx                                        |  385 +++----
 rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportMeta.java                    |   49 
 rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java                             |  354 ++++++
 rsf-server/src/main/java/com/vincent/rsf/server/manager/service/WarehouseAreasItemService.java          |    7 
 rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryResponse.java           |   52 +
 rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasItemController.java    |   97 +
 rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportColumnMeta.java              |   40 
 rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryRequest.java            |  165 +++
 rsf-admin/src/page/components/listReport/listReportUtils.js                                             |  162 +++
 rsf-admin/src/page/components/listReport/MyPrintButton.jsx                                              |   38 
 rsf-server/src/main/java/com/vincent/rsf/server/common/support/report/ListReportSupport.java            |  107 ++
 rsf-admin/src/page/components/listReport/ListReportActions.jsx                                          |   58 +
 18 files changed, 2,749 insertions(+), 280 deletions(-)

diff --git a/rsf-admin/src/page/components/MyExportButton.jsx b/rsf-admin/src/page/components/MyExportButton.jsx
index a684e47..1e132f1 100644
--- a/rsf-admin/src/page/components/MyExportButton.jsx
+++ b/rsf-admin/src/page/components/MyExportButton.jsx
@@ -1,5 +1,4 @@
 import * as React from "react";
-import { useCallback } from "react";
 import DownloadIcon from "@mui/icons-material/GetApp";
 import {
   Button,
@@ -8,6 +7,11 @@
   useListContext,
   useUnselectAll,
 } from "react-admin";
+import { useListReportActionParams } from "./listReport/useListReportOutput";
+import {
+  downloadBlobFile,
+  resolveReportMeta,
+} from "./listReport/listReportUtils";
 
 const MyExportButton = (props) => {
   const {
@@ -18,62 +22,73 @@
     icon = defaultIcon,
     exporter: customExporter,
     meta,
+    reportConfig,
+    onExport,
+    loading = false,
+    filename,
     ...rest
   } = props;
 
   const { filter, selectedIds, filterValues, resource, sort, total } = useListContext();
+  const { visibleColumns, params } = useListReportActionParams(reportConfig, { ids });
   const unSelect = useUnselectAll(resource);
   const dataProvider = useDataProvider();
   const notify = useNotify();
-  const handleClick = 
-  // useCallback(
-    (event) => {
-      dataProvider
-        .export(resource, {
+  const handleClick = async (event) => {
+    try {
+      const hasReportConfig = Boolean(reportConfig?.resource && Array.isArray(reportConfig?.columns));
+      if (hasReportConfig) {
+        const actionParams = {
+          ...params,
+          columns: visibleColumns,
+        };
+        if (typeof onExport === "function") {
+          await onExport(actionParams, event);
+        } else {
+          const resolvedResource = reportConfig.resource || params.resource;
+          const response = await dataProvider.export(resolvedResource, {
+            sort: actionParams.sort,
+            ids: actionParams.ids,
+            filter: actionParams.filter
+              ? { ...actionParams.filterValues, ...actionParams.filter }
+              : actionParams.filterValues,
+            pagination: { page: 1, perPage: maxResults },
+            meta,
+            columns: visibleColumns,
+            reportMeta: resolveReportMeta(reportConfig, actionParams),
+          });
+          downloadBlobFile(
+            response,
+            filename || reportConfig.exportFileName || `${resolvedResource}.xlsx`,
+          );
+        }
+      } else {
+        const response = await dataProvider.export(resource, {
           sort,
-          ids: selectedIds,
+          ids: ids ?? selectedIds,
           filter: filter ? { ...filterValues, ...filter } : filterValues,
           pagination: { page: 1, perPage: maxResults },
           meta,
-        })
-        .then((res) => {
-          const url = window.URL.createObjectURL(
-            new Blob([res.data], { type: res.headers["content-type"] }),
-          );
-          const link = document.createElement("a");
-          link.href = url;
-          link.setAttribute("download", `${resource}.xlsx`);
-          document.body.appendChild(link);
-          link.click();
-          link.remove();
-          unSelect();
-        })
-        .catch((error) => {
-          console.error(error);
-          notify("ra.notification.http_error", { type: "error" });
         });
+        downloadBlobFile(response, filename || `${resource}.xlsx`);
+      }
+      unSelect();
       if (typeof onClick === "function") {
         onClick(event);
       }
+    } catch (error) {
+      console.error(error);
+      notify("ra.notification.http_error", { type: "error" });
     }
-    // [
-    //   dataProvider,
-    //   filter,
-    //   filterValues,
-    //   maxResults,
-    //   notify,
-    //   onClick,
-    //   resource,
-    //   sort,
-    //   meta,
-    // ],
-  // );
+  };
+
+  const disabled = total === 0 || loading || (reportConfig?.columns && visibleColumns.length === 0);
 
   return (
     <Button
       onClick={handleClick}
       label={label}
-      disabled={total === 0}
+      disabled={disabled}
       {...sanitizeRestProps(rest)}
     >
       {icon}
diff --git a/rsf-admin/src/page/components/listReport/ListReportActions.jsx b/rsf-admin/src/page/components/listReport/ListReportActions.jsx
new file mode 100644
index 0000000..c87b95f
--- /dev/null
+++ b/rsf-admin/src/page/components/listReport/ListReportActions.jsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { FilterButton, SelectColumnsButton, TopToolbar } from "react-admin";
+import MyExportButton from "@/page/components/MyExportButton";
+import MyPrintButton from "./MyPrintButton";
+
+export const ListReportActions = ({
+    reportConfig,
+    loading = false,
+    onExport,
+    onPrintPreview,
+    showFilterButton = true,
+    showSelectColumnsButton = true,
+    children,
+}) => (
+    <TopToolbar>
+        {showFilterButton && <FilterButton />}
+        {showSelectColumnsButton && reportConfig?.preferenceKey && (
+            <SelectColumnsButton preferenceKey={reportConfig.preferenceKey} />
+        )}
+        {reportConfig?.enablePrint && (
+            <MyPrintButton
+                reportConfig={reportConfig}
+                onPrintPreview={onPrintPreview}
+                loading={loading}
+            />
+        )}
+        <MyExportButton
+            reportConfig={reportConfig}
+            onExport={onExport}
+            loading={loading}
+        />
+        {children}
+    </TopToolbar>
+);
+
+export const ListReportBulkActions = ({
+    reportConfig,
+    loading = false,
+    onExport,
+    onPrintPreview,
+}) => (
+    <>
+        {reportConfig?.enablePrint && (
+            <MyPrintButton
+                reportConfig={reportConfig}
+                onPrintPreview={onPrintPreview}
+                loading={loading}
+            />
+        )}
+        <MyExportButton
+            reportConfig={reportConfig}
+            onExport={onExport}
+            loading={loading}
+        />
+    </>
+);
+
+export default ListReportActions;
diff --git a/rsf-admin/src/page/components/listReport/ListReportPreviewDialog.jsx b/rsf-admin/src/page/components/listReport/ListReportPreviewDialog.jsx
new file mode 100644
index 0000000..1c95531
--- /dev/null
+++ b/rsf-admin/src/page/components/listReport/ListReportPreviewDialog.jsx
@@ -0,0 +1,607 @@
+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 ? "姝e湪鐢熸垚棰勮..." : "鏆傛棤鍙墦鍗版暟鎹�"}
+                                  </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
+                                ? `姝e湪鍔犺浇瀹屾暣鎵撳嵃鏁版嵁 ${loadedTransportPages}/${Math.max(totalTransportPages, 1)}`
+                                : "姝e湪鎸夌焊寮犲昂瀵搁噸鏂板垎椤�"}
+                        </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;
diff --git a/rsf-admin/src/page/components/listReport/MyPrintButton.jsx b/rsf-admin/src/page/components/listReport/MyPrintButton.jsx
new file mode 100644
index 0000000..038b48f
--- /dev/null
+++ b/rsf-admin/src/page/components/listReport/MyPrintButton.jsx
@@ -0,0 +1,38 @@
+import React from "react";
+import PrintOutlinedIcon from "@mui/icons-material/PrintOutlined";
+import { Button } from "react-admin";
+import { useListReportActionParams } from "./useListReportOutput";
+
+const MyPrintButton = ({
+    reportConfig,
+    onPrintPreview,
+    label = "toolbar.print",
+    icon = <PrintOutlinedIcon />,
+    loading = false,
+    disabled,
+    ...rest
+}) => {
+    const { visibleColumns, params } = useListReportActionParams(reportConfig);
+    const resolvedDisabled = disabled ?? (
+        params.total === 0 ||
+        loading ||
+        visibleColumns.length === 0
+    );
+
+    if (!reportConfig?.enablePrint) {
+        return null;
+    }
+
+    return (
+        <Button
+            onClick={() => onPrintPreview({ ...params, columns: visibleColumns })}
+            label={label}
+            disabled={resolvedDisabled}
+            {...rest}
+        >
+            {icon}
+        </Button>
+    );
+};
+
+export default MyPrintButton;
diff --git a/rsf-admin/src/page/components/listReport/listReportUtils.js b/rsf-admin/src/page/components/listReport/listReportUtils.js
new file mode 100644
index 0000000..e938b5e
--- /dev/null
+++ b/rsf-admin/src/page/components/listReport/listReportUtils.js
@@ -0,0 +1,162 @@
+import * as Common from "@/utils/common";
+
+export const DEFAULT_PRINT_PREVIEW_FETCH_PAGE_SIZE = 100;
+
+export const downloadBlobFile = (response, filename) => {
+    const blob = new Blob([response.data], { type: response.headers["content-type"] });
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.setAttribute("download", filename);
+    document.body.appendChild(link);
+    link.click();
+    link.remove();
+    window.URL.revokeObjectURL(url);
+};
+
+export const buildDefaultReportMeta = title => {
+    let user = null;
+    try {
+        const persistedUser = localStorage.getItem("user");
+        user = persistedUser ? JSON.parse(persistedUser) : null;
+    } catch (error) {
+        console.warn("Failed to parse persisted user", error);
+    }
+
+    const tenant = user?.tenant || {};
+    const now = new Date();
+    const year = now.getFullYear();
+    const month = `${now.getMonth() + 1}`.padStart(2, "0");
+    const day = `${now.getDate()}`.padStart(2, "0");
+
+    return {
+        companyName:
+            tenant?.name ||
+            tenant?.tenantName ||
+            tenant?.label ||
+            user?.fullName ||
+            "RSF",
+        printedBy: user?.fullName || user?.username || "绯荤粺鐢ㄦ埛",
+        reportDate: `${year}骞�${month}鏈�${day}鏃,
+        reportDateValue: `${year}-${month}-${day}`,
+        title,
+    };
+};
+
+export const buildListReportRequestPayload = ({
+    filter,
+    filterValues,
+    sort,
+    ids,
+    columns,
+    reportMeta,
+    extraPayload = {},
+}) => ({
+    ...Common.integrateParams({
+        sort,
+        filter: filter ? { ...filterValues, ...filter } : filterValues,
+    }),
+    ids: ids?.length ? ids : undefined,
+    columns,
+    reportMeta,
+    ...extraPayload,
+});
+
+const getColumnToken = column => column?.key || column?.source;
+
+const resolveColumnByToken = (columnDefs, token) => {
+    if (typeof token === "number" || /^\d+$/.test(String(token))) {
+        return columnDefs[Number(token)];
+    }
+
+    if (typeof token === "string") {
+        return columnDefs.find(column => column.key === token || column.source === token);
+    }
+
+    if (token && typeof token === "object") {
+        if ("index" in token) {
+            return columnDefs[Number(token.index)];
+        }
+        if ("source" in token) {
+            return columnDefs.find(column => column.source === token.source);
+        }
+        if ("key" in token) {
+            return columnDefs.find(column => column.key === token.key);
+        }
+        if ("id" in token) {
+            return columnDefs.find(column => column.key === token.id || column.source === token.id);
+        }
+    }
+
+    return undefined;
+};
+
+const buildDefaultVisibleColumns = (columnDefs, omitSet) =>
+    columnDefs.filter(column => !omitSet.has(column.source) && !omitSet.has(column.key));
+
+export const resolveVisibleColumns = ({
+    columnDefs = [],
+    storedColumns = [],
+    availableColumns = [],
+    storedOmit = [],
+    omit = [],
+}) => {
+    if (!Array.isArray(columnDefs) || columnDefs.length === 0) {
+        return [];
+    }
+
+    const omitSet = new Set([...(omit || []), ...(storedOmit || [])]);
+    const fallbackColumns = buildDefaultVisibleColumns(columnDefs, omitSet);
+    const storedOrder = Array.isArray(storedColumns) && storedColumns.length > 0
+        ? storedColumns
+        : Array.isArray(availableColumns) && availableColumns.length > 0
+            ? availableColumns
+            : [];
+
+    const resolvedColumns = storedOrder
+        .map(token => resolveColumnByToken(columnDefs, token))
+        .filter(Boolean)
+        .filter(column => !omitSet.has(column.source) && !omitSet.has(column.key));
+
+    const visibleColumns = resolvedColumns.length > 0 ? resolvedColumns : fallbackColumns;
+
+    return visibleColumns.map(column => ({
+        key: column.key,
+        source: column.source,
+        label: column.label,
+        isExtendField: Boolean(column.isExtendField),
+    }));
+};
+
+const getNestedValue = (record, source) =>
+    source.split(".").reduce((accumulator, segment) => {
+        if (accumulator == null) {
+            return undefined;
+        }
+        return accumulator[segment];
+    }, record);
+
+export const getListReportCellValue = (record, source) => {
+    if (!record || !source) {
+        return "";
+    }
+
+    const extendFieldMatch = source.match(/^extendFields\.\[(.+)\]$/);
+    if (extendFieldMatch) {
+        return record?.extendFields?.[extendFieldMatch[1]] ?? "";
+    }
+
+    if (Object.prototype.hasOwnProperty.call(record, source)) {
+        return record[source] ?? "";
+    }
+
+    const value = source.includes(".") ? getNestedValue(record, source) : record[source];
+    return value ?? "";
+};
+
+export const resolveReportMeta = (reportConfig, params) => {
+    if (typeof reportConfig?.buildReportMeta === "function") {
+        return reportConfig.buildReportMeta(params);
+    }
+    return reportConfig?.reportMeta || null;
+};
diff --git a/rsf-admin/src/page/components/listReport/useListReportOutput.js b/rsf-admin/src/page/components/listReport/useListReportOutput.js
new file mode 100644
index 0000000..6f9950b
--- /dev/null
+++ b/rsf-admin/src/page/components/listReport/useListReportOutput.js
@@ -0,0 +1,292 @@
+import { useCallback, useMemo, useRef, useState } from "react";
+import { useListContext, useStore } from "react-admin";
+import request from "@/utils/request";
+import {
+    DEFAULT_PRINT_PREVIEW_FETCH_PAGE_SIZE,
+    downloadBlobFile,
+    resolveReportMeta,
+    resolveVisibleColumns,
+} from "./listReportUtils";
+
+const getPreferencePath = (preferenceKey, field) =>
+    `preferences.${preferenceKey || "__listReport__"}.${field}`;
+
+export const useListReportActionParams = (reportConfig, options = {}) => {
+    const { ids: explicitIds } = options;
+    const { filter, filterValues, resource, selectedIds, sort, total } = useListContext();
+    const [storedColumns] = useStore(
+        getPreferencePath(reportConfig?.preferenceKey, "columns"),
+        []
+    );
+    const [availableColumns] = useStore(
+        getPreferencePath(reportConfig?.preferenceKey, "availableColumns"),
+        []
+    );
+    const [storedOmit] = useStore(
+        getPreferencePath(reportConfig?.preferenceKey, "omit"),
+        []
+    );
+
+    const visibleColumns = useMemo(
+        () =>
+            resolveVisibleColumns({
+                columnDefs: reportConfig?.columns,
+                storedColumns,
+                availableColumns,
+                storedOmit,
+                omit: reportConfig?.omit,
+            }),
+        [availableColumns, reportConfig?.columns, reportConfig?.omit, storedColumns, storedOmit]
+    );
+
+    return {
+        visibleColumns,
+        params: {
+            filter,
+            filterValues,
+            ids: explicitIds ?? selectedIds,
+            resource: reportConfig?.resource || resource,
+            sort,
+            total,
+        },
+    };
+};
+
+export const useListReportOutput = ({
+    reportConfig,
+    buildRequestPayload,
+    notify,
+}) => {
+    const [exportLoading, setExportLoading] = useState(false);
+    const [previewOpen, setPreviewOpen] = useState(false);
+    const [previewRows, setPreviewRows] = useState([]);
+    const [previewColumns, setPreviewColumns] = useState([]);
+    const [previewMeta, setPreviewMeta] = useState(null);
+    const [previewDataset, setPreviewDataset] = useState({
+        loadedPages: 0,
+        transportPages: 0,
+        pageSize: reportConfig?.previewPageSize || DEFAULT_PRINT_PREVIEW_FETCH_PAGE_SIZE,
+        total: 0,
+        fullyLoaded: false,
+    });
+    const [previewLoading, setPreviewLoading] = useState(false);
+    const [previewPrefetching, setPreviewPrefetching] = useState(false);
+    const requestIdRef = useRef(0);
+    const cacheRef = useRef(new Map());
+
+    const previewPageSize = reportConfig?.previewPageSize || DEFAULT_PRINT_PREVIEW_FETCH_PAGE_SIZE;
+
+    const buildPayload = useCallback(
+        params => {
+            const payload = typeof buildRequestPayload === "function"
+                ? buildRequestPayload(params)
+                : {};
+            if (payload.reportMeta) {
+                return payload;
+            }
+            return {
+                ...payload,
+                reportMeta: resolveReportMeta(reportConfig, params),
+            };
+        },
+        [buildRequestPayload, reportConfig]
+    );
+
+    const resetPreviewCache = useCallback(() => {
+        cacheRef.current = new Map();
+        setPreviewRows([]);
+        setPreviewColumns([]);
+        setPreviewMeta(null);
+        setPreviewDataset({
+            loadedPages: 0,
+            transportPages: 0,
+            pageSize: previewPageSize,
+            total: 0,
+            fullyLoaded: false,
+        });
+    }, [previewPageSize]);
+
+    const fetchPreviewPage = useCallback(
+        async ({ payload, current }) => {
+            const { data: { code, data, msg } } = await request.post(
+                `/${reportConfig.resource}/print/query`,
+                {
+                    ...payload,
+                    current,
+                    pageSize: previewPageSize,
+                }
+            );
+
+            if (code !== 200) {
+                throw new Error(msg || "Print preview query failed");
+            }
+
+            return data || {};
+        },
+        [previewPageSize, reportConfig.resource]
+    );
+
+    const syncPreviewCache = useCallback(
+        ({ totalPages, totalRows }) => {
+            const nextRows = [];
+            for (let pageIndex = 1; pageIndex <= totalPages; pageIndex += 1) {
+                const pageRows = cacheRef.current.get(pageIndex);
+                if (Array.isArray(pageRows)) {
+                    nextRows.push(...pageRows);
+                }
+            }
+
+            setPreviewRows(nextRows);
+            setPreviewDataset({
+                loadedPages: cacheRef.current.size,
+                transportPages: totalPages,
+                pageSize: previewPageSize,
+                total: totalRows,
+                fullyLoaded: cacheRef.current.size >= totalPages,
+            });
+        },
+        [previewPageSize]
+    );
+
+    const applyPreviewPage = useCallback(
+        ({ data, payload, openPreview = false }) => {
+            const current = Number(data.current) || 1;
+            const totalPages = Number(data.pages) || 0;
+            const totalRows = Number(data.total) || 0;
+
+            setPreviewColumns(payload.columns || []);
+            setPreviewMeta(payload.reportMeta || null);
+            cacheRef.current.set(current, Array.isArray(data.records) ? data.records : []);
+            syncPreviewCache({ totalPages, totalRows });
+
+            if (openPreview) {
+                setPreviewOpen(true);
+            }
+        },
+        [syncPreviewCache]
+    );
+
+    const prefetchPreviewPages = useCallback(
+        async ({ payload, totalPages, totalRows, requestId }) => {
+            if (totalPages <= 1) {
+                setPreviewPrefetching(false);
+                return;
+            }
+
+            setPreviewPrefetching(true);
+            const pendingPages = Array.from({ length: totalPages - 1 }, (_, index) => index + 2);
+            const workerCount = Math.min(3, pendingPages.length);
+            let hasErrored = false;
+
+            const worker = async () => {
+                while (pendingPages.length > 0 && requestIdRef.current === requestId) {
+                    const nextPage = pendingPages.shift();
+                    if (!nextPage) {
+                        return;
+                    }
+
+                    try {
+                        const data = await fetchPreviewPage({ payload, current: nextPage });
+                        if (requestIdRef.current !== requestId) {
+                            return;
+                        }
+                        cacheRef.current.set(nextPage, Array.isArray(data.records) ? data.records : []);
+                        syncPreviewCache({ totalPages, totalRows });
+                    } catch (error) {
+                        if (!hasErrored && requestIdRef.current === requestId) {
+                            hasErrored = true;
+                            console.error(error);
+                            notify(error.message || "ra.notification.http_error", { type: "error" });
+                        }
+                        return;
+                    }
+                }
+            };
+
+            await Promise.all(Array.from({ length: workerCount }, () => worker()));
+            if (requestIdRef.current === requestId) {
+                setPreviewPrefetching(false);
+            }
+        },
+        [fetchPreviewPage, notify, syncPreviewCache]
+    );
+
+    const openPrintPreview = useCallback(
+        async params => {
+            if (!reportConfig?.enablePrint) {
+                return;
+            }
+            setPreviewLoading(true);
+            try {
+                requestIdRef.current += 1;
+                const requestId = requestIdRef.current;
+                resetPreviewCache();
+                const payload = buildPayload(params);
+                const data = await fetchPreviewPage({ payload, current: 1 });
+                applyPreviewPage({ data, payload, openPreview: true });
+                prefetchPreviewPages({
+                    payload,
+                    totalPages: Number(data.pages) || 0,
+                    totalRows: Number(data.total) || 0,
+                    requestId,
+                });
+            } catch (error) {
+                console.error(error);
+                notify(error.message || "ra.notification.http_error", { type: "error" });
+            } finally {
+                setPreviewLoading(false);
+            }
+        },
+        [
+            applyPreviewPage,
+            buildPayload,
+            fetchPreviewPage,
+            notify,
+            prefetchPreviewPages,
+            reportConfig?.enablePrint,
+            resetPreviewCache,
+        ]
+    );
+
+    const closePrintPreview = useCallback(() => {
+        requestIdRef.current += 1;
+        setPreviewPrefetching(false);
+        setPreviewOpen(false);
+    }, []);
+
+    const exportReport = useCallback(
+        async params => {
+            setExportLoading(true);
+            try {
+                const payload = buildPayload(params);
+                const response = await request.post(`/${reportConfig.resource}/export`, payload, {
+                    responseType: "blob",
+                });
+                downloadBlobFile(
+                    response,
+                    reportConfig.exportFileName || `${reportConfig.resource}.xlsx`
+                );
+            } catch (error) {
+                console.error(error);
+                notify("ra.notification.http_error", { type: "error" });
+            } finally {
+                setExportLoading(false);
+            }
+        },
+        [buildPayload, notify, reportConfig.exportFileName, reportConfig.resource]
+    );
+
+    return {
+        exportLoading,
+        previewOpen,
+        previewRows,
+        previewColumns,
+        previewMeta,
+        previewDataset,
+        previewLoading,
+        previewPrefetching,
+        openPrintPreview,
+        closePrintPreview,
+        exportReport,
+    };
+};
diff --git a/rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemList.jsx b/rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemList.jsx
index 3e6af78..b12e6e7 100644
--- a/rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemList.jsx
+++ b/rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemList.jsx
@@ -1,267 +1,228 @@
-import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
-import { useNavigate } from 'react-router-dom';
+import React, { useEffect, useMemo, useState } from "react";
 import {
-    List,
     DatagridConfigurable,
-    SearchInput,
-    TopToolbar,
-    SelectColumnsButton,
-    EditButton,
-    FilterButton,
-    CreateButton,
-    ExportButton,
-    BulkDeleteButton,
-    WrapperField,
-    useRecordContext,
-    useTranslate,
-    useNotify,
+    List,
     useListContext,
-    FunctionField,
-    TextField,
-    NumberField,
-    DateField,
-    BooleanField,
-    ReferenceField,
-    TextInput,
-    DateTimeInput,
-    DateInput,
-    SelectInput,
-    NumberInput,
-    ReferenceInput,
-    ReferenceArrayInput,
-    useRefresh,
-    AutocompleteInput,
-    DeleteButton,
-} from 'react-admin';
-import { Box, Typography, Card, Stack, LinearProgress } from '@mui/material';
-import { styled } from '@mui/material/styles';
-import WarehouseAreasItemCreate from "./WarehouseAreasItemCreate";
-import WarehouseAreasItemPanel from "./WarehouseAreasItemPanel";
-import EmptyData from "../components/EmptyData";
-import request from '@/utils/request';
-import MyCreateButton from "../components/MyCreateButton";
-import MyExportButton from '../components/MyExportButton';
-import PageDrawer from "../components/PageDrawer";
-import MyField from "../components/MyField";
-import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
-import * as Common from '@/utils/common';
-import WarehouseIsptResult from "./WarehouseIsptResult"
+    useNotify,
+    useTranslate,
+} from "react-admin";
+import { Box, LinearProgress } from "@mui/material";
+import { styled } from "@mui/material/styles";
+import { DEFAULT_PAGE_SIZE } from "@/config/setting";
+import ListReportActions, {
+    ListReportBulkActions,
+} from "@/page/components/listReport/ListReportActions";
+import ListReportPreviewDialog from "@/page/components/listReport/ListReportPreviewDialog";
+import { useListReportOutput } from "@/page/components/listReport/useListReportOutput";
+import { buildListReportRequestPayload } from "@/page/components/listReport/listReportUtils";
+import request from "@/utils/request";
+import WarehouseIsptResult from "./WarehouseIsptResult";
+import {
+    buildWarehouseAreasItemBaseFilters,
+    buildWarehouseAreasItemDynamicFilters,
+    buildWarehouseAreasItemReportConfig,
+} from "./warehouseAreasItemOutputUtils.jsx";
 
-
-const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
-    '& .css-1vooibu-MuiSvgIcon-root': {
-        height: '.9em'
+const StyledDatagrid = styled(DatagridConfigurable)(() => ({
+    "& .css-1vooibu-MuiSvgIcon-root": {
+        height: ".9em",
     },
-    '& .RaDatagrid-row': {
-        cursor: 'auto'
+    "& .RaDatagrid-row": {
+        cursor: "auto",
     },
-    '& .column-name': {
+    "& .opt": {
+        width: 200,
     },
-    '& .opt': {
-        width: 200
+    "& .MuiTableCell-root": {
+        whiteSpace: "nowrap",
+        overflow: "visible",
+        textOverflow: "unset",
     },
-    '& .MuiTableCell-root': {
-    whiteSpace: 'nowrap',
-    overflow: 'visible',
-    textOverflow: 'unset'
-  }
 }));
-
-const filters = [
-    <SearchInput source="condition" alwaysOn />,
-    <NumberInput source="areaId" label="table.field.warehouseAreasItem.areaId" />,
-    <TextInput source="asnCode" label="table.field.warehouseAreasItem.asnCode" />,
-    <TextInput source="areaName" label="table.field.warehouseAreasItem.areaName" />,
-    <NumberInput source="matnrId" label="table.field.warehouseAreasItem.matnrId" />,
-    <TextInput source="matnrName" label="table.field.warehouseAreasItem.matnrName" />,
-    <TextInput source="matnrCode" label="table.field.warehouseAreasItem.matnrCode" />,
-    <TextInput source="barcode" label="table.field.warehouseAreasItem.barcode" />,
-    <NumberInput source="anfme" label="table.field.warehouseAreasItem.anfme" />,
-    <TextInput source="batch" label="table.field.warehouseAreasItem.batch" />,
-    <TextInput source="platOrderCode" label="table.field.asnOrderItem.platOrderCode" />,
-    <TextInput source="platWorkCode" label="table.field.asnOrderItem.platWorkCode" />,
-    <TextInput source="projectCode" label="table.field.asnOrderItem.projectCode" />,
-    <TextInput source="unit" label="table.field.warehouseAreasItem.unit" />,
-    <TextInput source="stockUnit" label="table.field.warehouseAreasItem.stockUnit" />,
-    <TextInput source="brand" label="table.field.warehouseAreasItem.brand" />,
-    <ReferenceInput source="shipperId" label="table.field.warehouseAreasItem.shipperId" reference="companys">
-        <AutocompleteInput label="table.field.warehouseAreasItem.shipperId" optionText="name" filterToQuery={(val) => ({ name: val })} />
-    </ReferenceInput>,
-    <TextInput source="splrId" label="table.field.warehouseAreasItem.splrId" />,
-    <NumberInput source="weight" label="table.field.warehouseAreasItem.weight" />,
-    <TextInput source="prodTime" label="table.field.warehouseAreasItem.prodTime" />,
-    <TextInput source="splrBtch" label="table.field.warehouseAreasItem.splrBtch" />,
-
-    <TextInput label="common.field.memo" source="memo" />,
-    <SelectInput
-        label="common.field.status"
-        source="status"
-        choices={[
-            { id: '1', name: 'common.enums.statusTrue' },
-            { id: '0', name: 'common.enums.statusFalse' },
-        ]}
-        resettable
-    />,
-]
 
 const WarehouseAreasItemList = () => {
     const translate = useTranslate();
-    const [itemInfo, setItemInfo] = useState({})
-    const [createDialog, setCreateDialog] = useState(false);
+    const notify = useNotify();
+    const [itemInfo] = useState({});
     const [drawerVal, setDrawerVal] = useState(false);
+    const { dynamicFields, dynamicFieldsLoading } = useWarehouseAreasItemDynamicFields(notify);
+
+    const filters = useMemo(
+        () => [
+            ...buildWarehouseAreasItemBaseFilters(),
+            ...buildWarehouseAreasItemDynamicFilters(dynamicFields),
+        ],
+        [dynamicFields]
+    );
+
+    const reportConfig = useMemo(
+        () => buildWarehouseAreasItemReportConfig(translate, dynamicFields),
+        [dynamicFields, translate]
+    );
+
+    const buildOutputPayload = params => buildListReportRequestPayload({
+        ...params,
+        reportMeta: reportConfig.buildReportMeta?.(params),
+    });
+
+    const {
+        exportLoading,
+        previewOpen,
+        previewRows,
+        previewColumns,
+        previewMeta,
+        previewDataset,
+        previewLoading,
+        previewPrefetching,
+        openPrintPreview,
+        closePrintPreview,
+        exportReport,
+    } = useListReportOutput({
+        reportConfig,
+        buildRequestPayload: buildOutputPayload,
+        notify,
+    });
+
+    const reportLoading = dynamicFieldsLoading || exportLoading || previewLoading;
 
     return (
         <Box display="flex">
             <List
-                title={"menu.warehouseAreasItem"}
+                title="menu.warehouseAreasItem"
                 empty={false}
                 filters={filters}
                 sort={{ field: "create_time", order: "desc" }}
                 sx={{
                     flexGrow: 1,
-                    transition: (theme) =>
-                        theme.transitions.create(['all'], {
+                    transition: theme =>
+                        theme.transitions.create(["all"], {
                             duration: theme.transitions.duration.enteringScreen,
                         }),
                 }}
                 actions={(
-                    <TopToolbar>
-                        <FilterButton />
-                        <SelectColumnsButton preferenceKey='warehouseAreasItem' />
-                        <MyExportButton />
-                    </TopToolbar>
+                    <ListReportActions
+                        reportConfig={reportConfig}
+                        loading={reportLoading}
+                        onExport={exportReport}
+                        onPrintPreview={openPrintPreview}
+                    />
                 )}
                 perPage={DEFAULT_PAGE_SIZE}
             >
                 <DynamicFields
-                    drawerVal={drawerVal}
-                    setDrawerVal={setDrawerVal}
-                    itemInfo={itemInfo}
-                    setItemInfo={setItemInfo} />
+                    reportConfig={reportConfig}
+                    loading={dynamicFieldsLoading}
+                    onExport={exportReport}
+                    onPrintPreview={openPrintPreview}
+                />
             </List>
-            <WarehouseAreasItemCreate
-                open={createDialog}
-                setOpen={setCreateDialog}
-            />
+
             <WarehouseIsptResult
                 record={itemInfo}
                 drawerVal={drawerVal}
                 from="warehosueItem"
                 setDrawerVal={setDrawerVal}
-            >
-            </WarehouseIsptResult>
-            {/* <PageDrawer
-                title='WarehouseAreasItem Detail'
-                drawerVal={drawerVal}
-                setDrawerVal={setDrawerVal}
-            >
-            </PageDrawer> */}
+            />
+
+            <ListReportPreviewDialog
+                open={previewOpen}
+                onClose={closePrintPreview}
+                rows={previewRows}
+                columns={previewColumns}
+                reportMeta={previewMeta}
+                totalRows={previewDataset.total}
+                loading={previewLoading}
+                allRowsLoaded={previewDataset.fullyLoaded}
+                loadedTransportPages={previewDataset.loadedPages}
+                totalTransportPages={previewDataset.transportPages}
+                prefetching={previewPrefetching}
+                dialogTitle="鏀惰揣搴撳瓨鎵撳嵃棰勮"
+                defaultOrientation={reportConfig.defaultOrientation}
+            />
         </Box>
-    )
-}
+    );
+};
 
 export default WarehouseAreasItemList;
 
-
-const DynamicFields = (props) => {
-    const { drawerVal, setDrawerVal, itemInfo, setItemInfo } = props
-    const translate = useTranslate();
-    const notify = useNotify();
-    const [columns, setColumns] = useState([]);
+const DynamicFields = ({ reportConfig, loading, onExport, onPrintPreview }) => {
     const { isLoading } = useListContext();
-    const refresh = useRefresh();
-    useEffect(() => {
-        getDynamicFields();
-    }, []);
-
-    const getDynamicFields = async () => {
-        const { data: { code, data, msg }, } = await request.get("/fields/enable/list");
-        if (code == 200) {
-            const arr = [
-                <NumberField key="id" source="id" />,
-                // <NumberField key="areaId" source="areaId" label="table.field.warehouseAreasItem.areaId" />,
-                <TextField key="areaName" source="areaName" label="鏀惰揣鍖哄悕绉�" />,    //table.field.warehouseAreasItem.areaName
-                <TextField key="asnCode" source="asnCode" label="table.field.warehouseAreasItem.asnCode" />,
-                <TextField source="platWorkCode" label="table.field.asnOrderItem.platWorkCode" />, 
-                <TextField source="platItemId" label="table.field.deliveryItem.platItemId" />, 
-                <NumberField key="matnrId" source="matnrId" label="table.field.warehouseAreasItem.matnrId" />,
-                <TextField key="matnrCode" source="matnrCode" label="table.field.warehouseAreasItem.matnrCode" />,
-                <TextField key="maktx" source="maktx" label="table.field.warehouseAreasItem.matnrName" />,
-                <TextField key="splrBatch" source="splrBatch" label="table.field.warehouseAreasItem.splrBtch" />,
-                <TextField key="batch" source="batch" label="table.field.warehouseAreasItem.batch" />,
-                <TextField key="trackCode" source="trackCode" label="table.field.warehouseAreasItem.barcode" />,
-                <TextField key="unit" source="unit" label="table.field.warehouseAreasItem.unit" />,
-                <NumberField key="anfme" source="anfme" label="table.field.warehouseAreasItem.anfme" />,
-                <NumberField key="workQty" source="workQty" label="table.field.warehouseAreasItem.workQty" />,
-                <NumberField key="ableQty" source="ableQty" label="table.field.warehouseAreasItem.qty" />,
-                <TextField source="platOrderCode" label="table.field.asnOrderItem.platOrderCode" />,                
-                <TextField source="projectCode" label="table.field.asnOrderItem.projectCode" />,
-                // <MyField source="isptQty" label="table.field.qlyIsptItem.anfme"
-                //     onClick={(event, record, val) => {
-                //         event.stopPropagation();
-                //         setItemInfo(record)
-                //         setDrawerVal(!!drawerVal && drawerVal === val ? null : val);
-                //     }}
-                // />,
-                                
-                
-                // <TextField key="stockUnit" source="stockUnit" label="table.field.warehouseAreasItem.stockUnit" />,
-                <TextField key="brand" source="brand" label="table.field.warehouseAreasItem.brand" />,
-                <TextField key="shipperId" source="shipperId" label="table.field.warehouseAreasItem.shipperId" />,
-                <TextField key="splrId" source="splrId" label="table.field.warehouseAreasItem.splrId" />,
-                <TextField key="isptResult" source="isptResult$" label="table.field.warehouseAreasItem.isptResult" sortable={false} />,
-                <NumberField key="weight" source="weight" label="table.field.warehouseAreasItem.weight" />,
-                <TextField key="prodTime" source="prodTime" label="table.field.warehouseAreasItem.prodTime" />,
-            ]
-            const fields = data.map(el => <TextField key={el.fields} source={`extendFields.[${el.fields}]`} label={el.fieldsAlise} />)
-            const lastArr = [
-                <TextField key="updateBy" source="updateBy$" label="common.field.updateBy" />,
-                <DateField key="updateTime" source="updateTime" label="common.field.updateTime" showTime />,                
-                <TextField key="createBy" source="createBy$" label="common.field.createBy" />,
-                <DateField key="createTime" source="createTime" label="common.field.createTime" showTime />,
-                <BooleanField key="statusBool" source="statusBool" label="common.field.status" sortable={false} />,
-                <TextField key="memo" source="memo" label="common.field.memo" sortable={false} />,
-            ]
-            setColumns([...arr, ...fields, ...lastArr]);
-            //filters娣诲姞杩囨护瀛楁
-            data.map(el => {
-                var i = 0;
-                filters.map((item) => {
-                    if (item.key === el.fields) {
-                        i = 1;
-                    }
-                })
-                i === 0 && filters.push(<TextInput key={el.fields} source={el.fields} label={el.fieldsAlise} />)
-            })
-        } else {
-            notify(msg);
-        }
-    }
 
     return (
-        <Box sx={{ position: 'relative', minHeight: "82vh", }}>
-            {isLoading && (
+        <Box sx={{ position: "relative", minHeight: "82vh" }}>
+            {(isLoading || loading) && (
                 <LinearProgress
                     sx={{
                         height: "2px",
-                        position: 'absolute',
+                        position: "absolute",
                         top: 0,
                         left: 0,
                         right: 0,
                     }}
                 />
             )}
-            {columns.length > 0 &&
+
+            {reportConfig.columns.length > 0 && (
                 <StyledDatagrid
-                    preferenceKey='warehouseAreasItem'
-                    bulkActionButtons={false}
-                    rowClick={(id, resource, record) => false}
-                    omit={['prodTime','platOrderCode','id', 'createTime', 'memo', 'areaId', 'brand', 
-                         'weight', 'splrId', 'projectCode','statusBool', 'extendFields.[priceUnitId]', 'isptResult$', 'extendFields.[inStockType]',
-                         'matnrId', 'trackCode', 'workQty', 'batch', 'shipperId', 'isptResult', 'createBy$', 'createTime', 'extendFields.[baseUnitId]']}
+                    preferenceKey={reportConfig.preferenceKey}
+                    bulkActionButtons={(
+                        <ListReportBulkActions
+                            reportConfig={reportConfig}
+                            loading={loading}
+                            onExport={onExport}
+                            onPrintPreview={onPrintPreview}
+                        />
+                    )}
+                    rowClick={false}
+                    omit={reportConfig.omit}
                 >
-                    {columns.map((column) => column)}
-                </StyledDatagrid>}
+                    {reportConfig.columns.map(column => column.element)}
+                </StyledDatagrid>
+            )}
         </Box>
-    )
-}
+    );
+};
+
+const useWarehouseAreasItemDynamicFields = notify => {
+    const [dynamicFields, setDynamicFields] = useState([]);
+    const [dynamicFieldsLoading, setDynamicFieldsLoading] = useState(true);
+
+    useEffect(() => {
+        let active = true;
+
+        const loadDynamicFields = async () => {
+            setDynamicFieldsLoading(true);
+            try {
+                const { data: { code, data, msg } } = await request.get("/fields/enable/list");
+                if (!active) {
+                    return;
+                }
+                if (code === 200) {
+                    setDynamicFields(Array.isArray(data) ? data : []);
+                    return;
+                }
+                notify(msg, { type: "warning" });
+            } catch (error) {
+                if (!active) {
+                    return;
+                }
+                console.error(error);
+                notify("ra.notification.http_error", { type: "error" });
+            } finally {
+                if (active) {
+                    setDynamicFieldsLoading(false);
+                }
+            }
+        };
+
+        loadDynamicFields();
+        return () => {
+            active = false;
+        };
+    }, [notify]);
+
+    return {
+        dynamicFields,
+        dynamicFieldsLoading,
+    };
+};
diff --git a/rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemPrintPreview.jsx b/rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemPrintPreview.jsx
new file mode 100644
index 0000000..e44be32
--- /dev/null
+++ b/rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemPrintPreview.jsx
@@ -0,0 +1 @@
+export { default } from "@/page/components/listReport/ListReportPreviewDialog";
diff --git a/rsf-admin/src/page/warehouseAreasItem/warehouseAreasItemOutputUtils.jsx b/rsf-admin/src/page/warehouseAreasItem/warehouseAreasItemOutputUtils.jsx
new file mode 100644
index 0000000..325533e
--- /dev/null
+++ b/rsf-admin/src/page/warehouseAreasItem/warehouseAreasItemOutputUtils.jsx
@@ -0,0 +1,493 @@
+import React from "react";
+import {
+    BooleanField,
+    DateField,
+    NumberField,
+    SearchInput,
+    SelectInput,
+    TextField,
+    TextInput,
+    NumberInput,
+    ReferenceInput,
+    AutocompleteInput,
+} from "react-admin";
+import { buildDefaultReportMeta } from "@/page/components/listReport/listReportUtils";
+
+export const WAREHOUSE_AREAS_ITEM_PREFERENCE_KEY = "warehouseAreasItem";
+
+export const WAREHOUSE_AREAS_ITEM_OMIT = [
+    "prodTime",
+    "platOrderCode",
+    "id",
+    "createTime",
+    "memo",
+    "areaId",
+    "brand",
+    "weight",
+    "splrId",
+    "projectCode",
+    "statusBool",
+    "extendFields.[priceUnitId]",
+    "isptResult$",
+    "extendFields.[inStockType]",
+    "matnrId",
+    "trackCode",
+    "workQty",
+    "batch",
+    "shipperId",
+    "isptResult",
+    "createBy$",
+    "createTime",
+    "extendFields.[baseUnitId]",
+];
+
+const resolveLabel = (translate, label, fallback = "") => {
+    if (!label) {
+        return fallback;
+    }
+    const translated = translate(label, { _: label });
+    return translated || fallback || label;
+};
+
+export const buildWarehouseAreasItemReportMeta = () => {
+    return {
+        ...buildDefaultReportMeta("鏀惰揣搴撳瓨鎶ヨ〃"),
+    };
+};
+
+export const buildWarehouseAreasItemBaseFilters = () => ([
+    <SearchInput key="condition" source="condition" alwaysOn />,
+    <NumberInput key="areaId" source="areaId" label="table.field.warehouseAreasItem.areaId" />,
+    <TextInput key="asnCode" source="asnCode" label="table.field.warehouseAreasItem.asnCode" />,
+    <TextInput key="areaName" source="areaName" label="table.field.warehouseAreasItem.areaName" />,
+    <NumberInput key="matnrId" source="matnrId" label="table.field.warehouseAreasItem.matnrId" />,
+    <TextInput key="matnrName" source="matnrName" label="table.field.warehouseAreasItem.matnrName" />,
+    <TextInput key="matnrCode" source="matnrCode" label="table.field.warehouseAreasItem.matnrCode" />,
+    <TextInput key="barcode" source="barcode" label="table.field.warehouseAreasItem.barcode" />,
+    <NumberInput key="anfme" source="anfme" label="table.field.warehouseAreasItem.anfme" />,
+    <TextInput key="batch" source="batch" label="table.field.warehouseAreasItem.batch" />,
+    <TextInput key="platOrderCode" source="platOrderCode" label="table.field.asnOrderItem.platOrderCode" />,
+    <TextInput key="platWorkCode" source="platWorkCode" label="table.field.asnOrderItem.platWorkCode" />,
+    <TextInput key="projectCode" source="projectCode" label="table.field.asnOrderItem.projectCode" />,
+    <TextInput key="unit" source="unit" label="table.field.warehouseAreasItem.unit" />,
+    <TextInput key="stockUnit" source="stockUnit" label="table.field.warehouseAreasItem.stockUnit" />,
+    <TextInput key="brand" source="brand" label="table.field.warehouseAreasItem.brand" />,
+    <ReferenceInput
+        key="shipperId"
+        source="shipperId"
+        label="table.field.warehouseAreasItem.shipperId"
+        reference="companys"
+    >
+        <AutocompleteInput
+            label="table.field.warehouseAreasItem.shipperId"
+            optionText="name"
+            filterToQuery={val => ({ name: val })}
+        />
+    </ReferenceInput>,
+    <TextInput key="splrId" source="splrId" label="table.field.warehouseAreasItem.splrId" />,
+    <NumberInput key="weight" source="weight" label="table.field.warehouseAreasItem.weight" />,
+    <TextInput key="prodTime" source="prodTime" label="table.field.warehouseAreasItem.prodTime" />,
+    <TextInput key="splrBtch" source="splrBtch" label="table.field.warehouseAreasItem.splrBtch" />,
+    <TextInput key="memo" label="common.field.memo" source="memo" />,
+    <SelectInput
+        key="status"
+        label="common.field.status"
+        source="status"
+        choices={[
+            { id: "1", name: "common.enums.statusTrue" },
+            { id: "0", name: "common.enums.statusFalse" },
+        ]}
+        resettable
+    />,
+]);
+
+const createColumnDef = ({ key, source, label, element, isExtendField = false }) => ({
+    key,
+    source,
+    label,
+    isExtendField,
+    element,
+});
+
+export const buildWarehouseAreasItemColumnDefs = (translate, dynamicFields = []) => {
+    const baseDefs = [
+        createColumnDef({
+            key: "id",
+            source: "id",
+            label: "ID",
+            element: <NumberField key="id" source="id" label="ID" />,
+        }),
+        createColumnDef({
+            key: "areaName",
+            source: "areaName",
+            label: "鏀惰揣鍖哄悕绉�",
+            element: <TextField key="areaName" source="areaName" label="鏀惰揣鍖哄悕绉�" />,
+        }),
+        createColumnDef({
+            key: "asnCode",
+            source: "asnCode",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.asnCode"),
+            element: (
+                <TextField
+                    key="asnCode"
+                    source="asnCode"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.asnCode")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "platWorkCode",
+            source: "platWorkCode",
+            label: resolveLabel(translate, "table.field.asnOrderItem.platWorkCode"),
+            element: (
+                <TextField
+                    key="platWorkCode"
+                    source="platWorkCode"
+                    label={resolveLabel(translate, "table.field.asnOrderItem.platWorkCode")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "platItemId",
+            source: "platItemId",
+            label: resolveLabel(translate, "table.field.deliveryItem.platItemId"),
+            element: (
+                <TextField
+                    key="platItemId"
+                    source="platItemId"
+                    label={resolveLabel(translate, "table.field.deliveryItem.platItemId")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "matnrId",
+            source: "matnrId",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.matnrId"),
+            element: (
+                <NumberField
+                    key="matnrId"
+                    source="matnrId"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.matnrId")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "matnrCode",
+            source: "matnrCode",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.matnrCode"),
+            element: (
+                <TextField
+                    key="matnrCode"
+                    source="matnrCode"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.matnrCode")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "maktx",
+            source: "maktx",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.matnrName"),
+            element: (
+                <TextField
+                    key="maktx"
+                    source="maktx"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.matnrName")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "splrBatch",
+            source: "splrBatch",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.splrBtch"),
+            element: (
+                <TextField
+                    key="splrBatch"
+                    source="splrBatch"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.splrBtch")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "batch",
+            source: "batch",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.batch"),
+            element: (
+                <TextField
+                    key="batch"
+                    source="batch"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.batch")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "trackCode",
+            source: "trackCode",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.barcode"),
+            element: (
+                <TextField
+                    key="trackCode"
+                    source="trackCode"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.barcode")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "unit",
+            source: "unit",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.unit"),
+            element: (
+                <TextField
+                    key="unit"
+                    source="unit"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.unit")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "anfme",
+            source: "anfme",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.anfme"),
+            element: (
+                <NumberField
+                    key="anfme"
+                    source="anfme"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.anfme")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "workQty",
+            source: "workQty",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.workQty"),
+            element: (
+                <NumberField
+                    key="workQty"
+                    source="workQty"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.workQty")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "ableQty",
+            source: "ableQty",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.qty"),
+            element: (
+                <NumberField
+                    key="ableQty"
+                    source="ableQty"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.qty")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "platOrderCode",
+            source: "platOrderCode",
+            label: resolveLabel(translate, "table.field.asnOrderItem.platOrderCode"),
+            element: (
+                <TextField
+                    key="platOrderCode"
+                    source="platOrderCode"
+                    label={resolveLabel(translate, "table.field.asnOrderItem.platOrderCode")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "projectCode",
+            source: "projectCode",
+            label: resolveLabel(translate, "table.field.asnOrderItem.projectCode"),
+            element: (
+                <TextField
+                    key="projectCode"
+                    source="projectCode"
+                    label={resolveLabel(translate, "table.field.asnOrderItem.projectCode")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "brand",
+            source: "brand",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.brand"),
+            element: (
+                <TextField
+                    key="brand"
+                    source="brand"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.brand")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "shipperId",
+            source: "shipperId",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.shipperId"),
+            element: (
+                <TextField
+                    key="shipperId"
+                    source="shipperId"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.shipperId")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "splrId",
+            source: "splrId",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.splrId"),
+            element: (
+                <TextField
+                    key="splrId"
+                    source="splrId"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.splrId")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "isptResult$",
+            source: "isptResult$",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.isptResult"),
+            element: (
+                <TextField
+                    key="isptResult$"
+                    source="isptResult$"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.isptResult")}
+                    sortable={false}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "weight",
+            source: "weight",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.weight"),
+            element: (
+                <NumberField
+                    key="weight"
+                    source="weight"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.weight")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "prodTime",
+            source: "prodTime",
+            label: resolveLabel(translate, "table.field.warehouseAreasItem.prodTime"),
+            element: (
+                <TextField
+                    key="prodTime"
+                    source="prodTime"
+                    label={resolveLabel(translate, "table.field.warehouseAreasItem.prodTime")}
+                />
+            ),
+        }),
+    ];
+
+    const extendDefs = dynamicFields.map(field => {
+        const source = `extendFields.[${field.fields}]`;
+        return createColumnDef({
+            key: source,
+            source,
+            label: field.fieldsAlise || field.fields,
+            isExtendField: true,
+            element: <TextField key={source} source={source} label={field.fieldsAlise || field.fields} />,
+        });
+    });
+
+    const tailDefs = [
+        createColumnDef({
+            key: "updateBy$",
+            source: "updateBy$",
+            label: resolveLabel(translate, "common.field.updateBy"),
+            element: (
+                <TextField
+                    key="updateBy$"
+                    source="updateBy$"
+                    label={resolveLabel(translate, "common.field.updateBy")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "updateTime",
+            source: "updateTime",
+            label: resolveLabel(translate, "common.field.updateTime"),
+            element: (
+                <DateField
+                    key="updateTime"
+                    source="updateTime"
+                    label={resolveLabel(translate, "common.field.updateTime")}
+                    showTime
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "createBy$",
+            source: "createBy$",
+            label: resolveLabel(translate, "common.field.createBy"),
+            element: (
+                <TextField
+                    key="createBy$"
+                    source="createBy$"
+                    label={resolveLabel(translate, "common.field.createBy")}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "createTime",
+            source: "createTime",
+            label: resolveLabel(translate, "common.field.createTime"),
+            element: (
+                <DateField
+                    key="createTime"
+                    source="createTime"
+                    label={resolveLabel(translate, "common.field.createTime")}
+                    showTime
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "statusBool",
+            source: "statusBool",
+            label: resolveLabel(translate, "common.field.status"),
+            element: (
+                <BooleanField
+                    key="statusBool"
+                    source="statusBool"
+                    label={resolveLabel(translate, "common.field.status")}
+                    sortable={false}
+                />
+            ),
+        }),
+        createColumnDef({
+            key: "memo",
+            source: "memo",
+            label: resolveLabel(translate, "common.field.memo"),
+            element: (
+                <TextField
+                    key="memo"
+                    source="memo"
+                    label={resolveLabel(translate, "common.field.memo")}
+                    sortable={false}
+                />
+            ),
+        }),
+    ];
+
+    return [...baseDefs, ...extendDefs, ...tailDefs];
+};
+
+export const buildWarehouseAreasItemDynamicFilters = dynamicFields =>
+    dynamicFields.map(field => (
+        <TextInput
+            key={`filter-${field.fields}`}
+            source={field.fields}
+            label={field.fieldsAlise || field.fields}
+        />
+    ));
+
+export const buildWarehouseAreasItemReportConfig = (translate, dynamicFields = []) => ({
+    resource: "warehouseAreasItem",
+    preferenceKey: WAREHOUSE_AREAS_ITEM_PREFERENCE_KEY,
+    title: "鏀惰揣搴撳瓨鎶ヨ〃",
+    columns: buildWarehouseAreasItemColumnDefs(translate, dynamicFields),
+    omit: WAREHOUSE_AREAS_ITEM_OMIT,
+    buildReportMeta: buildWarehouseAreasItemReportMeta,
+    enablePrint: true,
+    defaultOrientation: "landscape",
+    exportFileName: "warehouseAreasItem.xlsx",
+});
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportColumnMeta.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportColumnMeta.java
new file mode 100644
index 0000000..3f12565
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportColumnMeta.java
@@ -0,0 +1,40 @@
+package com.vincent.rsf.server.common.domain.report;
+
+public class ReportColumnMeta {
+    private String key;
+    private String source;
+    private String label;
+    private Boolean isExtendField;
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getSource() {
+        return source;
+    }
+
+    public void setSource(String source) {
+        this.source = source;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    public Boolean getIsExtendField() {
+        return isExtendField;
+    }
+
+    public void setIsExtendField(Boolean extendField) {
+        isExtendField = extendField;
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportMeta.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportMeta.java
new file mode 100644
index 0000000..c88b389
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportMeta.java
@@ -0,0 +1,49 @@
+package com.vincent.rsf.server.common.domain.report;
+
+public class ReportMeta {
+    private String title;
+    private String companyName;
+    private String printedBy;
+    private String reportDate;
+    private String reportDateValue;
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getCompanyName() {
+        return companyName;
+    }
+
+    public void setCompanyName(String companyName) {
+        this.companyName = companyName;
+    }
+
+    public String getPrintedBy() {
+        return printedBy;
+    }
+
+    public void setPrintedBy(String printedBy) {
+        this.printedBy = printedBy;
+    }
+
+    public String getReportDate() {
+        return reportDate;
+    }
+
+    public void setReportDate(String reportDate) {
+        this.reportDate = reportDate;
+    }
+
+    public String getReportDateValue() {
+        return reportDateValue;
+    }
+
+    public void setReportDateValue(String reportDateValue) {
+        this.reportDateValue = reportDateValue;
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryRequest.java
new file mode 100644
index 0000000..6bbf09a
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryRequest.java
@@ -0,0 +1,165 @@
+package com.vincent.rsf.server.common.domain.report;
+
+import java.util.*;
+
+public class ReportQueryRequest {
+    private Long current = 1L;
+    private Long pageSize = 12L;
+    private String orderBy;
+    private List<Long> ids = new ArrayList<>();
+    private List<ReportColumnMeta> columns = new ArrayList<>();
+    private ReportMeta reportMeta;
+    private Map<String, Object> rawParams = new HashMap<>();
+
+    public static ReportQueryRequest fromMap(Map<String, Object> map) {
+        ReportQueryRequest request = new ReportQueryRequest();
+        Map<String, Object> safeMap = map == null ? new HashMap<>() : new HashMap<>(map);
+        request.setRawParams(safeMap);
+        request.setCurrent(parseLong(safeMap.get("current"), 1L));
+        request.setPageSize(parseLong(safeMap.get("pageSize"), 12L));
+        request.setOrderBy(Objects.isNull(safeMap.get("orderBy")) ? null : String.valueOf(safeMap.get("orderBy")));
+        request.setIds(parseIds(safeMap.get("ids")));
+        request.setColumns(parseColumns(safeMap.get("columns")));
+        request.setReportMeta(parseReportMeta(safeMap.get("reportMeta")));
+        return request;
+    }
+
+    public Map<String, Object> toPageParamMap(boolean includeFilters) {
+        Map<String, Object> queryMap = new HashMap<>(rawParams);
+        queryMap.remove("ids");
+        queryMap.remove("columns");
+        queryMap.remove("reportMeta");
+
+        if (!includeFilters) {
+            Object resolvedOrderBy = queryMap.get("orderBy");
+            queryMap.clear();
+            if (!Objects.isNull(resolvedOrderBy)) {
+                queryMap.put("orderBy", resolvedOrderBy);
+            }
+        }
+
+        return queryMap;
+    }
+
+    public Long getCurrent() {
+        return current;
+    }
+
+    public void setCurrent(Long current) {
+        this.current = current;
+    }
+
+    public Long getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(Long pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public String getOrderBy() {
+        return orderBy;
+    }
+
+    public void setOrderBy(String orderBy) {
+        this.orderBy = orderBy;
+    }
+
+    public List<Long> getIds() {
+        return ids;
+    }
+
+    public void setIds(List<Long> ids) {
+        this.ids = ids;
+    }
+
+    public List<ReportColumnMeta> getColumns() {
+        return columns;
+    }
+
+    public void setColumns(List<ReportColumnMeta> columns) {
+        this.columns = columns;
+    }
+
+    public ReportMeta getReportMeta() {
+        return reportMeta;
+    }
+
+    public void setReportMeta(ReportMeta reportMeta) {
+        this.reportMeta = reportMeta;
+    }
+
+    public Map<String, Object> getRawParams() {
+        return rawParams;
+    }
+
+    public void setRawParams(Map<String, Object> rawParams) {
+        this.rawParams = rawParams;
+    }
+
+    private static long parseLong(Object value, long defaultValue) {
+        if (Objects.isNull(value)) {
+            return defaultValue;
+        }
+        try {
+            return Long.parseLong(String.valueOf(value));
+        } catch (NumberFormatException ignore) {
+            return defaultValue;
+        }
+    }
+
+    private static List<Long> parseIds(Object idsObj) {
+        List<Long> ids = new ArrayList<>();
+        if (!(idsObj instanceof Collection<?> collection)) {
+            return ids;
+        }
+        for (Object value : collection) {
+            if (Objects.isNull(value)) {
+                continue;
+            }
+            ids.add(Long.parseLong(String.valueOf(value)));
+        }
+        return ids;
+    }
+
+    private static List<ReportColumnMeta> parseColumns(Object columnsObj) {
+        List<ReportColumnMeta> columns = new ArrayList<>();
+        if (!(columnsObj instanceof Collection<?> collection)) {
+            return columns;
+        }
+        for (Object item : collection) {
+            if (!(item instanceof Map<?, ?> columnMap)) {
+                continue;
+            }
+            Object source = columnMap.get("source");
+            if (Objects.isNull(source) || String.valueOf(source).trim().isEmpty()) {
+                continue;
+            }
+            ReportColumnMeta columnMeta = new ReportColumnMeta();
+            columnMeta.setKey(Objects.isNull(columnMap.get("key")) ? null : String.valueOf(columnMap.get("key")));
+            columnMeta.setSource(String.valueOf(source));
+            columnMeta.setLabel(Objects.isNull(columnMap.get("label")) ? String.valueOf(source) : String.valueOf(columnMap.get("label")));
+            columnMeta.setIsExtendField(Boolean.parseBoolean(String.valueOf(columnMap.get("isExtendField"))));
+            columns.add(columnMeta);
+        }
+        return columns;
+    }
+
+    private static ReportMeta parseReportMeta(Object reportMetaObj) {
+        if (!(reportMetaObj instanceof Map<?, ?> reportMetaMap)) {
+            return null;
+        }
+        ReportMeta reportMeta = new ReportMeta();
+        reportMeta.setTitle(getMapString(reportMetaMap, "title"));
+        reportMeta.setCompanyName(getMapString(reportMetaMap, "companyName"));
+        reportMeta.setPrintedBy(getMapString(reportMetaMap, "printedBy"));
+        reportMeta.setReportDate(getMapString(reportMetaMap, "reportDate"));
+        reportMeta.setReportDateValue(getMapString(reportMetaMap, "reportDateValue"));
+        return reportMeta;
+    }
+
+    private static String getMapString(Map<?, ?> map, String key) {
+        Object value = map.get(key);
+        return Objects.isNull(value) ? null : String.valueOf(value);
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryResponse.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryResponse.java
new file mode 100644
index 0000000..62f7a55
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryResponse.java
@@ -0,0 +1,52 @@
+package com.vincent.rsf.server.common.domain.report;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReportQueryResponse<T> {
+    private List<T> records = new ArrayList<>();
+    private Long total = 0L;
+    private Long current = 1L;
+    private Long pageSize = 0L;
+    private Long pages = 0L;
+
+    public List<T> getRecords() {
+        return records;
+    }
+
+    public void setRecords(List<T> records) {
+        this.records = records;
+    }
+
+    public Long getTotal() {
+        return total;
+    }
+
+    public void setTotal(Long total) {
+        this.total = total;
+    }
+
+    public Long getCurrent() {
+        return current;
+    }
+
+    public void setCurrent(Long current) {
+        this.current = current;
+    }
+
+    public Long getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(Long pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public Long getPages() {
+        return pages;
+    }
+
+    public void setPages(Long pages) {
+        this.pages = pages;
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/support/report/ListReportSupport.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/support/report/ListReportSupport.java
new file mode 100644
index 0000000..93aa553
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/support/report/ListReportSupport.java
@@ -0,0 +1,107 @@
+package com.vincent.rsf.server.common.support.report;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.vincent.rsf.framework.common.Cools;
+import com.vincent.rsf.server.common.domain.report.ReportColumnMeta;
+import com.vincent.rsf.server.common.domain.report.ReportMeta;
+import com.vincent.rsf.server.common.domain.report.ReportQueryRequest;
+import com.vincent.rsf.server.common.domain.report.ReportQueryResponse;
+import com.vincent.rsf.server.common.utils.ExcelUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ListReportSupport<T> {
+
+    public interface QueryWrapperBuilder<T> {
+        QueryWrapper<T> build(ReportQueryRequest request);
+    }
+
+    public interface RecordLoader<T> {
+        List<T> list(QueryWrapper<T> queryWrapper);
+
+        IPage<T> page(Page<T> page, QueryWrapper<T> queryWrapper);
+
+        default void afterLoad(List<T> records) {
+        }
+    }
+
+    private final QueryWrapperBuilder<T> queryWrapperBuilder;
+    private final RecordLoader<T> recordLoader;
+
+    public ListReportSupport(QueryWrapperBuilder<T> queryWrapperBuilder, RecordLoader<T> recordLoader) {
+        this.queryWrapperBuilder = queryWrapperBuilder;
+        this.recordLoader = recordLoader;
+    }
+
+    public List<T> queryRecords(ReportQueryRequest request) {
+        List<T> records = recordLoader.list(queryWrapperBuilder.build(request));
+        recordLoader.afterLoad(records);
+        return records;
+    }
+
+    public ReportQueryResponse<T> queryPage(ReportQueryRequest request) {
+        Page<T> page = new Page<>(
+                Math.max(request.getCurrent(), 1L),
+                Math.max(request.getPageSize(), 1L)
+        );
+        IPage<T> result = recordLoader.page(page, queryWrapperBuilder.build(request));
+        recordLoader.afterLoad(result.getRecords());
+
+        ReportQueryResponse<T> response = new ReportQueryResponse<>();
+        response.setRecords(result.getRecords());
+        response.setTotal(result.getTotal());
+        response.setCurrent(result.getCurrent());
+        response.setPageSize(result.getSize());
+        response.setPages(result.getPages());
+        return response;
+    }
+
+    public static List<ExcelUtil.ColumnMeta> toExcelColumns(List<ReportColumnMeta> columns) {
+        if (columns == null || columns.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<ExcelUtil.ColumnMeta> excelColumns = new ArrayList<>();
+        for (ReportColumnMeta column : columns) {
+            if (column == null || Cools.isEmpty(column.getSource())) {
+                continue;
+            }
+            excelColumns.add(new ExcelUtil.ColumnMeta()
+                    .setKey(column.getKey())
+                    .setSource(column.getSource())
+                    .setLabel(column.getLabel())
+                    .setExtendField(Boolean.TRUE.equals(column.getIsExtendField())));
+        }
+        return excelColumns;
+    }
+
+    public static ExcelUtil.ReportMeta toExcelReportMeta(ReportMeta reportMeta) {
+        if (reportMeta == null) {
+            return null;
+        }
+        return new ExcelUtil.ReportMeta()
+                .setTitle(reportMeta.getTitle())
+                .setCompanyName(reportMeta.getCompanyName())
+                .setPrintedBy(reportMeta.getPrintedBy())
+                .setReportDate(reportMeta.getReportDate())
+                .setReportDateValue(reportMeta.getReportDateValue());
+    }
+
+    public static void applyOrderBy(QueryWrapper<?> queryWrapper, String orderBy) {
+        if (Cools.isEmpty(orderBy)) {
+            return;
+        }
+        for (String item : orderBy.split(",")) {
+            String[] temp = item.trim().split(" ");
+            if (temp.length == 0 || Cools.isEmpty(temp[0])) {
+                continue;
+            }
+            boolean asc = temp.length == 1 || !"desc".equalsIgnoreCase(temp[temp.length - 1]);
+            queryWrapper.orderBy(true, asc, temp[0]);
+        }
+    }
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java
index 8d81944..1c12cc5 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java
@@ -15,10 +15,12 @@
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.poi.ss.util.CellRangeAddress;
 
 import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
@@ -131,6 +133,262 @@
         return workbook;
     }
 
+    public static Workbook create(List<?> list, List<ColumnMeta> columns) {
+        return create(list, columns, null);
+    }
+
+    public static Workbook create(List<?> list, List<ColumnMeta> columns, ReportMeta reportMeta) {
+        XSSFWorkbook workbook = new XSSFWorkbook();
+        Sheet sheet = workbook.createSheet("export");
+        List<ColumnMeta> safeColumns = columns == null ? Collections.emptyList() : columns;
+        int sheetColumnCount = safeColumns.size() + 1;
+        configureA4PrintLayout(sheet);
+
+        CellStyle titleStyle = createTitleStyle(workbook);
+        CellStyle metaLabelStyle = createMetaLabelStyle(workbook);
+        CellStyle metaValueStyle = createMetaValueStyle(workbook);
+        CellStyle headerStyle = createHeaderStyle(workbook);
+        CellStyle bodyStyle = createBodyStyle(workbook);
+        CellStyle serialStyle = createCenteredBodyStyle(workbook);
+
+        int rowIndex = 0;
+        if (reportMeta != null) {
+            Row titleRow = sheet.createRow(rowIndex++);
+            titleRow.setHeightInPoints(28);
+            Cell titleCell = titleRow.createCell(0);
+            titleCell.setCellValue(StringUtils.defaultIfBlank(reportMeta.getTitle(), "鎶ヨ〃"));
+            titleCell.setCellStyle(titleStyle);
+            sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, Math.max(0, sheetColumnCount - 1)));
+
+            Row metaRow = sheet.createRow(rowIndex++);
+            int metaCol = 0;
+            metaCol = writeMetaPair(metaRow, metaCol, "鎶ヨ〃鏃ユ湡", reportMeta.getReportDate(), metaLabelStyle, metaValueStyle);
+            writeMetaPair(metaRow, metaCol, "鎵撳嵃浜�", reportMeta.getPrintedBy(), metaLabelStyle, metaValueStyle);
+
+            rowIndex++;
+        }
+
+        int headerRowIndex = rowIndex;
+        Row header = sheet.createRow(rowIndex++);
+        Cell serialHeaderCell = header.createCell(0);
+        serialHeaderCell.setCellValue("搴忓彿");
+        serialHeaderCell.setCellStyle(headerStyle);
+
+        for (int i = 0; i < safeColumns.size(); i++) {
+            ColumnMeta column = safeColumns.get(i);
+            Cell headerCell = header.createCell(i + 1);
+            headerCell.setCellValue(
+                    StringUtils.isBlank(column.getLabel()) ? column.getSource() : column.getLabel()
+            );
+            headerCell.setCellStyle(headerStyle);
+        }
+
+        if (list != null) {
+            int serialNo = 1;
+            for (Object rowObj : list) {
+                Row row = sheet.createRow(rowIndex++);
+                Cell serialCell = row.createCell(0);
+                serialCell.setCellValue(String.format("%03d", serialNo++));
+                serialCell.setCellStyle(serialStyle);
+                for (int i = 0; i < safeColumns.size(); i++) {
+                    Object value = getColumnValue(rowObj, safeColumns.get(i).getSource());
+                    Cell cell = row.createCell(i + 1);
+                    cell.setCellStyle(bodyStyle);
+                    if (value != null) {
+                        if (value instanceof Date) {
+                            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                            cell.setCellValue(sdf.format((Date) value));
+                        } else {
+                            cell.setCellValue(value.toString());
+                        }
+                    }
+                }
+            }
+        }
+
+        for (int i = 0; i <= safeColumns.size(); i++) {
+            sheet.autoSizeColumn(i);
+            sheet.setColumnWidth(i, Math.min(sheet.getColumnWidth(i) + 1024, 12000));
+        }
+
+        sheet.setRepeatingRows(CellRangeAddress.valueOf((headerRowIndex + 1) + ":" + (headerRowIndex + 1)));
+
+        return workbook;
+    }
+
+    private static void configureA4PrintLayout(Sheet sheet) {
+        sheet.setAutobreaks(true);
+        sheet.setFitToPage(true);
+        sheet.setHorizontallyCenter(true);
+        sheet.setDisplayGridlines(false);
+
+        PrintSetup printSetup = sheet.getPrintSetup();
+        printSetup.setPaperSize(PrintSetup.A4_PAPERSIZE);
+        printSetup.setLandscape(true);
+        printSetup.setFitWidth((short) 1);
+        printSetup.setFitHeight((short) 0);
+
+        sheet.setMargin(Sheet.LeftMargin, 0.3);
+        sheet.setMargin(Sheet.RightMargin, 0.3);
+        sheet.setMargin(Sheet.TopMargin, 0.4);
+        sheet.setMargin(Sheet.BottomMargin, 0.4);
+    }
+
+    private static int writeMetaPair(Row row, int startCol, String label, String value, CellStyle labelStyle, CellStyle valueStyle) {
+        Cell labelCell = row.createCell(startCol);
+        labelCell.setCellValue(label + "锛�");
+        labelCell.setCellStyle(labelStyle);
+
+        Cell valueCell = row.createCell(startCol + 1);
+        valueCell.setCellValue(StringUtils.defaultString(value));
+        valueCell.setCellStyle(valueStyle);
+        return startCol + 2;
+    }
+
+    private static CellStyle createTitleStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setBorderBottom(BorderStyle.THICK);
+        Font font = workbook.createFont();
+        font.setBold(true);
+        font.setFontHeightInPoints((short) 16);
+        style.setFont(font);
+        return style;
+    }
+
+    private static CellStyle createMetaLabelStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setAlignment(HorizontalAlignment.LEFT);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setBorderBottom(BorderStyle.THIN);
+        Font font = workbook.createFont();
+        font.setBold(true);
+        style.setFont(font);
+        return style;
+    }
+
+    private static CellStyle createMetaValueStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setAlignment(HorizontalAlignment.LEFT);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setBorderBottom(BorderStyle.THIN);
+        return style;
+    }
+
+    private static CellStyle createHeaderStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+        Font font = workbook.createFont();
+        font.setBold(true);
+        style.setFont(font);
+        return style;
+    }
+
+    private static CellStyle createBodyStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setAlignment(HorizontalAlignment.LEFT);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        return style;
+    }
+
+    private static CellStyle createCenteredBodyStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        return style;
+    }
+
+    private static Object getColumnValue(Object rowObj, String source) {
+        if (rowObj == null || StringUtils.isBlank(source)) {
+            return null;
+        }
+
+        if (rowObj instanceof Map) {
+            return getValueFromMap((Map<?, ?>) rowObj, source);
+        }
+
+        String extendFieldKey = extractExtendFieldKey(source);
+        if (extendFieldKey != null) {
+            Object extendFields = getBeanValue(rowObj, "extendFields");
+            if (extendFields instanceof Map) {
+                return ((Map<?, ?>) extendFields).get(extendFieldKey);
+            }
+            return null;
+        }
+
+        return getBeanValue(rowObj, source);
+    }
+
+    private static Object getValueFromMap(Map<?, ?> rowObj, String source) {
+        String extendFieldKey = extractExtendFieldKey(source);
+        if (extendFieldKey != null) {
+            Object extendFields = rowObj.get("extendFields");
+            if (extendFields instanceof Map) {
+                return ((Map<?, ?>) extendFields).get(extendFieldKey);
+            }
+            return null;
+        }
+        return rowObj.get(source);
+    }
+
+    private static String extractExtendFieldKey(String source) {
+        if (source == null || !source.startsWith("extendFields.[")) {
+            return null;
+        }
+        int startIndex = source.indexOf('[');
+        int endIndex = source.indexOf(']');
+        if (startIndex < 0 || endIndex <= startIndex) {
+            return null;
+        }
+        return source.substring(startIndex + 1, endIndex);
+    }
+
+    private static Object getBeanValue(Object rowObj, String source) {
+        Object value = invokeGetter(rowObj, source);
+        if (value != null) {
+            return value;
+        }
+        Field field = findField(rowObj.getClass(), source);
+        if (field == null) {
+            return null;
+        }
+        try {
+            field.setAccessible(true);
+            return field.get(rowObj);
+        } catch (IllegalAccessException ignore) {
+            return null;
+        }
+    }
+
+    private static Object invokeGetter(Object target, String source) {
+        String suffix = Character.toUpperCase(source.charAt(0)) + source.substring(1);
+        String[] methodNames = new String[] { "get" + suffix, "is" + suffix };
+        for (String methodName : methodNames) {
+            try {
+                Method method = target.getClass().getMethod(methodName);
+                return method.invoke(target);
+            } catch (Exception ignore) {
+            }
+        }
+        return null;
+    }
+
+    private static Field findField(Class<?> clazz, String source) {
+        Class<?> current = clazz;
+        while (current != null && current != Object.class) {
+            try {
+                return current.getDeclaredField(source);
+            } catch (NoSuchFieldException ignore) {
+                current = current.getSuperclass();
+            }
+        }
+        return null;
+    }
+
     /**
      * 娣诲姞瀵煎叆excel閰嶇疆鍙傛暟
      * 娉細榛樿閰嶇疆鍙弧瓒冲綋鍓嶉渶姹�
@@ -223,6 +481,102 @@
         return false;
     }
 
+    public static class ColumnMeta {
+        private String key;
+        private String source;
+        private String label;
+        private Boolean extendField;
+
+        public String getKey() {
+            return key;
+        }
+
+        public ColumnMeta setKey(String key) {
+            this.key = key;
+            return this;
+        }
+
+        public String getSource() {
+            return source;
+        }
+
+        public ColumnMeta setSource(String source) {
+            this.source = source;
+            return this;
+        }
+
+        public String getLabel() {
+            return label;
+        }
+
+        public ColumnMeta setLabel(String label) {
+            this.label = label;
+            return this;
+        }
+
+        public Boolean getExtendField() {
+            return extendField;
+        }
+
+        public ColumnMeta setExtendField(Boolean extendField) {
+            this.extendField = extendField;
+            return this;
+        }
+    }
+
+    public static class ReportMeta {
+        private String title;
+        private String companyName;
+        private String printedBy;
+        private String reportDate;
+        private String reportDateValue;
+
+        public String getTitle() {
+            return title;
+        }
+
+        public ReportMeta setTitle(String title) {
+            this.title = title;
+            return this;
+        }
+
+        public String getCompanyName() {
+            return companyName;
+        }
+
+        public ReportMeta setCompanyName(String companyName) {
+            this.companyName = companyName;
+            return this;
+        }
+
+        public String getReportDate() {
+            return reportDate;
+        }
+
+        public ReportMeta setReportDate(String reportDate) {
+            this.reportDate = reportDate;
+            return this;
+        }
+
+        public String getReportDateValue() {
+            return reportDateValue;
+        }
+
+        public ReportMeta setReportDateValue(String reportDateValue) {
+            this.reportDateValue = reportDateValue;
+            return this;
+        }
+
+        public String getPrintedBy() {
+            return printedBy;
+        }
+
+        public ReportMeta setPrintedBy(String printedBy) {
+            this.printedBy = printedBy;
+            return this;
+        }
+    }
+
 
 }
 
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasItemController.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasItemController.java
index f468a2d..94d2207 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasItemController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasItemController.java
@@ -11,6 +11,9 @@
 import com.vincent.rsf.server.common.domain.BaseParam;
 import com.vincent.rsf.server.common.domain.KeyValVo;
 import com.vincent.rsf.server.common.domain.PageParam;
+import com.vincent.rsf.server.common.domain.report.ReportQueryRequest;
+import com.vincent.rsf.server.common.domain.report.ReportQueryResponse;
+import com.vincent.rsf.server.common.support.report.ListReportSupport;
 import com.vincent.rsf.server.common.utils.FieldsUtils;
 import com.vincent.rsf.server.manager.entity.WarehouseAreasItem;
 import com.vincent.rsf.server.manager.service.WarehouseAreasItemService;
@@ -33,21 +36,10 @@
     @PreAuthorize("hasAuthority('manager:warehouseAreasItem:list')")
     @PostMapping("/warehouseAreasItem/page")
     public R page(@RequestBody Map<String, Object> map) {
-        BaseParam baseParam = buildParam(map, BaseParam.class);
-        PageParam<WarehouseAreasItem, BaseParam> pageParam = new PageParam<>(baseParam, WarehouseAreasItem.class);
-        QueryWrapper<WarehouseAreasItem> queryWrapper = pageParam.buildWrapper(true);
-        /**鎷兼帴鎵╁睍瀛楁杩囨护*/
-        FieldsUtils.setFieldsFilters(queryWrapper,pageParam, WarehouseAreasItem.class);
-        /**鎷兼帴鎵╁睍瀛楁*/
+        PageParam<WarehouseAreasItem, BaseParam> pageParam = buildPageParam(map, true);
+        QueryWrapper<WarehouseAreasItem> queryWrapper = buildFilterQueryWrapper(pageParam);
         PageParam<WarehouseAreasItem, BaseParam> page = warehouseAreasItemService.page(pageParam, queryWrapper);
-        List<WarehouseAreasItem> records = page.getRecords();
-        for (WarehouseAreasItem record : records) {
-            if (!Objects.isNull(record.getFieldsIndex())) {
-                Map<String, String> fields = FieldsUtils.getFields(record.getFieldsIndex());
-                record.setExtendFields(fields);
-            }
-        }
-        page.setRecords(records);
+        warehouseAreasItemService.fillExtendFields(page.getRecords());
         return R.ok().add(page);
     }
 
@@ -55,19 +47,10 @@
     @PreAuthorize("hasAuthority('manager:warehouseAreasItem:list')")
     @PostMapping("/warehouseAreasItem/ispts/page")
     public R getIsptPage(@RequestBody Map<String, Object> map) {
-        BaseParam baseParam = buildParam(map, BaseParam.class);
-        PageParam<WarehouseAreasItem, BaseParam> pageParam = new PageParam<>(baseParam, WarehouseAreasItem.class);
+        PageParam<WarehouseAreasItem, BaseParam> pageParam = buildPageParam(map, true);
         QueryWrapper<WarehouseAreasItem> queryWrapper = pageParam.buildWrapper(true);
-        /**鎷兼帴鎵╁睍瀛楁*/
         IPage<WarehouseAreasItem> page = warehouseAreasItemService.pageByItemId(pageParam, queryWrapper);
-        List<WarehouseAreasItem> records = page.getRecords();
-        for (WarehouseAreasItem record : records) {
-            if (!Objects.isNull(record.getFieldsIndex())) {
-                Map<String, String> fields = FieldsUtils.getFields(record.getFieldsIndex());
-                record.setExtendFields(fields);
-            }
-        }
-        page.setRecords(records);
+        warehouseAreasItemService.fillExtendFields(page.getRecords());
         return R.ok().add(page);
     }
 
@@ -141,7 +124,69 @@
     @PreAuthorize("hasAuthority('manager:warehouseAreasItem:list')")
     @PostMapping("/warehouseAreasItem/export")
     public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
-        ExcelUtil.build(ExcelUtil.create(warehouseAreasItemService.list(), WarehouseAreasItem.class), response);
+        ReportQueryRequest request = ReportQueryRequest.fromMap(map);
+        List<WarehouseAreasItem> records = createListReportSupport().queryRecords(request);
+        List<ExcelUtil.ColumnMeta> columns = ListReportSupport.toExcelColumns(request.getColumns());
+        ExcelUtil.ReportMeta reportMeta = ListReportSupport.toExcelReportMeta(request.getReportMeta());
+        if (columns.isEmpty()) {
+            ExcelUtil.build(ExcelUtil.create(records, WarehouseAreasItem.class), response);
+            return;
+        }
+        ExcelUtil.build(ExcelUtil.create(records, columns, reportMeta), response);
+    }
+
+    @PreAuthorize("hasAuthority('manager:warehouseAreasItem:list')")
+    @PostMapping("/warehouseAreasItem/print/query")
+    public R printQuery(@RequestBody Map<String, Object> map) {
+        ReportQueryResponse<WarehouseAreasItem> result = createListReportSupport()
+                .queryPage(ReportQueryRequest.fromMap(map));
+        return R.ok().add(result);
+    }
+
+    private PageParam<WarehouseAreasItem, BaseParam> buildPageParam(Map<String, Object> map, boolean includeFilters) {
+        return buildPageParam(ReportQueryRequest.fromMap(map), includeFilters);
+    }
+
+    private PageParam<WarehouseAreasItem, BaseParam> buildPageParam(ReportQueryRequest request, boolean includeFilters) {
+        BaseParam baseParam = buildParam(request.toPageParamMap(includeFilters), BaseParam.class);
+        return new PageParam<>(baseParam, WarehouseAreasItem.class);
+    }
+
+    private QueryWrapper<WarehouseAreasItem> buildFilterQueryWrapper(PageParam<WarehouseAreasItem, BaseParam> pageParam) {
+        QueryWrapper<WarehouseAreasItem> queryWrapper = pageParam.buildWrapper(true);
+        FieldsUtils.setFieldsFilters(queryWrapper, pageParam, WarehouseAreasItem.class);
+        return queryWrapper;
+    }
+
+    private QueryWrapper<WarehouseAreasItem> buildOutputQueryWrapper(ReportQueryRequest request) {
+        List<Long> ids = request.getIds();
+        PageParam<WarehouseAreasItem, BaseParam> pageParam = buildPageParam(request, ids.isEmpty());
+        QueryWrapper<WarehouseAreasItem> queryWrapper = ids.isEmpty()
+                ? buildFilterQueryWrapper(pageParam)
+                : new QueryWrapper<>();
+
+        if (!ids.isEmpty()) {
+            queryWrapper.in("id", ids);
+        }
+        ListReportSupport.applyOrderBy(queryWrapper, pageParam.getWhere().getOrderBy());
+        return queryWrapper;
+    }
+
+    private ListReportSupport<WarehouseAreasItem> createListReportSupport() {
+        return new ListReportSupport<>(
+                this::buildOutputQueryWrapper,
+                new ListReportSupport.RecordLoader<>() {
+                    @Override
+                    public List<WarehouseAreasItem> list(QueryWrapper<WarehouseAreasItem> queryWrapper) {
+                        return warehouseAreasItemService.listForOutput(queryWrapper);
+                    }
+
+                    @Override
+                    public IPage<WarehouseAreasItem> page(Page<WarehouseAreasItem> page, QueryWrapper<WarehouseAreasItem> queryWrapper) {
+                        return warehouseAreasItemService.pageForOutput(page, queryWrapper);
+                    }
+                }
+        );
     }
 
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/WarehouseAreasItemService.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/WarehouseAreasItemService.java
index d2a4283..8d86f79 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/WarehouseAreasItemService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/WarehouseAreasItemService.java
@@ -3,6 +3,7 @@
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.vincent.rsf.server.common.domain.BaseParam;
 import com.vincent.rsf.server.common.domain.PageParam;
@@ -16,4 +17,10 @@
     List<WarehouseAreasItem> getList();
 
     IPage<WarehouseAreasItem> pageByItemId(PageParam<WarehouseAreasItem, BaseParam> pageParam, QueryWrapper<WarehouseAreasItem> queryWrapper);
+
+    List<WarehouseAreasItem> listForOutput(QueryWrapper<WarehouseAreasItem> queryWrapper);
+
+    IPage<WarehouseAreasItem> pageForOutput(Page<WarehouseAreasItem> page, QueryWrapper<WarehouseAreasItem> queryWrapper);
+
+    void fillExtendFields(List<WarehouseAreasItem> records);
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WarehouseAreasItemServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WarehouseAreasItemServiceImpl.java
index 3f9a887..7e6c220 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WarehouseAreasItemServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WarehouseAreasItemServiceImpl.java
@@ -2,6 +2,7 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.vincent.rsf.server.common.domain.BaseParam;
 import com.vincent.rsf.server.common.domain.PageParam;
 import com.vincent.rsf.server.common.utils.FieldsUtils;
@@ -20,12 +21,7 @@
     @Override
     public List<WarehouseAreasItem> getList() {
         List<WarehouseAreasItem> areasItems = this.list();
-        for (WarehouseAreasItem areasItem : areasItems) {
-            if (Objects.isNull(areasItem.getFieldsIndex())) {
-                continue;
-            }
-            areasItem.setExtendFields(FieldsUtils.getFields(areasItem.getFieldsIndex()));
-        }
+        fillExtendFields(areasItems);
         return areasItems;
     }
 
@@ -34,4 +30,31 @@
         IPage<WarehouseAreasItem> itemIPage = this.baseMapper.pageByItemId(pageParam, queryWrapper);
         return itemIPage;
     }
+
+    @Override
+    public List<WarehouseAreasItem> listForOutput(QueryWrapper<WarehouseAreasItem> queryWrapper) {
+        List<WarehouseAreasItem> records = this.list(queryWrapper);
+        fillExtendFields(records);
+        return records;
+    }
+
+    @Override
+    public IPage<WarehouseAreasItem> pageForOutput(Page<WarehouseAreasItem> page, QueryWrapper<WarehouseAreasItem> queryWrapper) {
+        IPage<WarehouseAreasItem> outputPage = this.page(page, queryWrapper);
+        fillExtendFields(outputPage.getRecords());
+        return outputPage;
+    }
+
+    @Override
+    public void fillExtendFields(List<WarehouseAreasItem> records) {
+        if (records == null || records.isEmpty()) {
+            return;
+        }
+        for (WarehouseAreasItem record : records) {
+            if (Objects.isNull(record.getFieldsIndex())) {
+                continue;
+            }
+            record.setExtendFields(FieldsUtils.getFields(record.getFieldsIndex()));
+        }
+    }
 }

--
Gitblit v1.9.1