zhou zhou
3 天以前 6e5ff559023efd2d24fdca2adcb7268d06420e46
#打印+导出
12个文件已添加
6个文件已修改
3029 ■■■■■ 已修改文件
rsf-admin/src/page/components/MyExportButton.jsx 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/listReport/ListReportActions.jsx 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/listReport/ListReportPreviewDialog.jsx 607 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/listReport/MyPrintButton.jsx 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/listReport/listReportUtils.js 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/listReport/useListReportOutput.js 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemList.jsx 385 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemPrintPreview.jsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/warehouseAreasItem/warehouseAreasItemOutputUtils.jsx 493 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportColumnMeta.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportMeta.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryRequest.java 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryResponse.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/support/report/ListReportSupport.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java 354 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasItemController.java 97 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/WarehouseAreasItemService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WarehouseAreasItemServiceImpl.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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}
rsf-admin/src/page/components/listReport/ListReportActions.jsx
New file
@@ -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;
rsf-admin/src/page/components/listReport/ListReportPreviewDialog.jsx
New file
@@ -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 ? "正在生成预览..." : "暂无可打印数据"}
                                  </Box>
                              )}
                    </Box>
                )}
            </DialogContent>
            <DialogActions sx={{ px: 3, py: 2, justifyContent: "space-between", gap: 2, flexWrap: "wrap" }}>
                <Box sx={{ display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
                    <ToggleButtonGroup
                        value={orientation}
                        exclusive
                        size="small"
                        onChange={(_, value) => {
                            if (value) {
                                setCurrentPage(1);
                                setOrientation(value);
                            }
                        }}
                    >
                        <ToggleButton value="landscape">横版</ToggleButton>
                        <ToggleButton value="portrait">竖版</ToggleButton>
                    </ToggleButtonGroup>
                    <Button
                        size="small"
                        onClick={() => setCurrentPage(page => Math.max(1, page - 1))}
                        disabled={loading || currentPage <= 1}
                    >
                        上一页
                    </Button>
                    <Typography variant="body2" sx={{ minWidth: 104, textAlign: "center" }}>
                        第 {paginatedRows.length === 0 ? 0 : currentPage} / {paginatedRows.length} 页
                    </Typography>
                    <Button
                        size="small"
                        onClick={() => setCurrentPage(page => Math.min(paginatedRows.length, page + 1))}
                        disabled={loading || paginatedRows.length === 0 || currentPage >= paginatedRows.length}
                    >
                        下一页
                    </Button>
                    {(prefetching || measuring) && (
                        <Typography variant="body2" sx={{ color: "#6b7280" }}>
                            {prefetching
                                ? `正在加载完整打印数据 ${loadedTransportPages}/${Math.max(totalTransportPages, 1)}`
                                : "正在按纸张尺寸重新分页"}
                        </Typography>
                    )}
                </Box>
                <Button
                    variant="contained"
                    startIcon={
                        isPreparingPrint || prefetching || measuring
                            ? <CircularProgress size={16} color="inherit" />
                            : <PrintOutlinedIcon />
                    }
                    onClick={() => setIsPreparingPrint(true)}
                    disabled={printDisabled}
                >
                    {printButtonLabel}
                </Button>
            </DialogActions>
            {open && rows.length > 0 && columns.length > 0 && (
                <Box
                    sx={{
                        position: "fixed",
                        left: "-20000px",
                        top: 0,
                        visibility: "hidden",
                        pointerEvents: "none",
                        backgroundColor: "#fff",
                    }}
                >
                    <ReportPageFrame paperSize={previewPaperSize} printable>
                        <Box ref={measurePageRef} sx={{ width: "100%", height: "100%" }}>
                            <Box ref={measureContentRef} sx={{ display: "flex", flexDirection: "column", gap: 2, width: "100%", height: "100%" }}>
                                <ReportHeader printedAt={printedAt} reportMeta={reportMeta} totalCount={totalCount} />
                                <ReportTable
                                    displayColumns={displayColumns}
                                    pageRows={rows}
                                    renderRow={(record, rowIndex) => renderDataRow(record, rowIndex, 0, "measure")}
                                    tableWrapRef={measureTableWrapRef}
                                    theadRef={measureTheadRef}
                                    tbodyRef={measureBodyRef}
                                />
                                <Box
                                    ref={measureFooterRef}
                                    sx={{
                                        display: "flex",
                                        justifyContent: "flex-end",
                                        color: "#6b7280",
                                        fontSize: 12,
                                    }}
                                >
                                    第 1 / 1 页
                                </Box>
                            </Box>
                        </Box>
                    </ReportPageFrame>
                </Box>
            )}
            {isPreparingPrint && (
                <Box
                    ref={printContentRef}
                    className="list-report-print-shell list-report-print-live-root"
                    sx={{
                        position: "fixed",
                        left: "-10000px",
                        top: 0,
                        backgroundColor: "#fff",
                        px: 3,
                        py: 3,
                    }}
                >
                    {paginatedRows.map((pageRows, pageIndex) =>
                        renderPage(pageRows, pageIndex, paginatedRows.length, true)
                    )}
                </Box>
            )}
        </Dialog>
    );
};
export default ListReportPreviewDialog;
rsf-admin/src/page/components/listReport/MyPrintButton.jsx
New file
@@ -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;
rsf-admin/src/page/components/listReport/listReportUtils.js
New file
@@ -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;
};
rsf-admin/src/page/components/listReport/useListReportOutput.js
New file
@@ -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,
    };
};
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,
    };
};
rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemPrintPreview.jsx
New file
@@ -0,0 +1 @@
export { default } from "@/page/components/listReport/ListReportPreviewDialog";
rsf-admin/src/page/warehouseAreasItem/warehouseAreasItemOutputUtils.jsx
New file
@@ -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",
});
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportColumnMeta.java
New file
@@ -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;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportMeta.java
New file
@@ -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;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryRequest.java
New file
@@ -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);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/report/ReportQueryResponse.java
New file
@@ -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;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/support/report/ListReportSupport.java
New file
@@ -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]);
        }
    }
}
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;
        }
    }
}
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);
                    }
                }
        );
    }
}
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);
}
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()));
        }
    }
}