zhou zhou
12 小时以前 44ac1c9a2a6ee6ac9f618f4a63510f8f94d1b1a9
#打印+导出
4个文件已添加
5个文件已修改
1648 ■■■■ 已修改文件
rsf-admin/src/page/components/ListExportPrintButton.jsx 545 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/listExportPrintUtils.js 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/warehouseAreas/WarehouseAreasList.jsx 313 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemList.jsx 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/service/ListExportHandler.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/service/ListExportService.java 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasController.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasItemController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/ListExportPrintButton.jsx
New file
@@ -0,0 +1,545 @@
import React, { useMemo, useRef, useState } from "react";
import DownloadIcon from "@mui/icons-material/GetApp";
import PrintOutlinedIcon from "@mui/icons-material/PrintOutlined";
import SaveIcon from '@mui/icons-material/Save';
import {
    Box,
    Button as MuiButton,
    CircularProgress,
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Paper,
    Table,
    TableBody,
    TableCell,
    TableContainer,
    TableHead,
    TableRow,
    Typography,
} from '@mui/material';
import {
    Button,
    useDataProvider,
    useListContext,
    useNotify,
    useStore,
    useTranslate,
    useUnselectAll,
} from "react-admin";
import { useReactToPrint } from "react-to-print";
import DialogCloseButton from "./DialogCloseButton";
import { getValueBySource, resolveVisibleColumns } from "./listExportPrintUtils";
const PREVIEW_ROW_LIMIT = 200;
const formatColumnLabel = (label, translate) => {
    if (!label) {
        return '';
    }
    if (typeof label !== 'string') {
        return String(label);
    }
    if (label.includes('.')) {
        return translate(label, { _: label });
    }
    return label;
};
const formatValue = (value, fieldType) => {
    if (value == null) {
        return '';
    }
    if (fieldType === 'boolean') {
        return value ? 'true' : 'false';
    }
    return String(value);
};
const padNumber = (value) => String(value).padStart(2, '0');
const formatDate = (date = new Date()) => (
    `${date.getFullYear()}年${padNumber(date.getMonth() + 1)}月${padNumber(date.getDate())}日`
);
const formatDateTime = (date = new Date()) => (
    `${date.getFullYear()}/${padNumber(date.getMonth() + 1)}/${padNumber(date.getDate())} ${padNumber(date.getHours())}:${padNumber(date.getMinutes())}:${padNumber(date.getSeconds())}`
);
const getPrintLayoutConfig = (columnCount) => {
    if (columnCount >= 16) {
        return {
            bodyFontSize: 8,
            headerFontSize: 8,
            cellPaddingX: 0.35,
            cellPaddingY: 0.4,
            sequenceWidth: 36,
        };
    }
    if (columnCount >= 12) {
        return {
            bodyFontSize: 9,
            headerFontSize: 9,
            cellPaddingX: 0.45,
            cellPaddingY: 0.5,
            sequenceWidth: 42,
        };
    }
    return {
        bodyFontSize: 10,
        headerFontSize: 10,
        cellPaddingX: 0.6,
        cellPaddingY: 0.65,
        sequenceWidth: 52,
    };
};
const getPrintOperator = () => {
    try {
        const user = JSON.parse(localStorage.getItem('user'));
        return user?.fullName || user?.username || 'SYSTEM';
    } catch (error) {
        return 'SYSTEM';
    }
};
const buildReportMeta = (reportTitle, count = 0) => ({
    reportTitle,
    reportDate: formatDate(),
    printedAt: formatDateTime(),
    operator: getPrintOperator(),
    count,
});
const getRecordKey = (record, index) => (
    record?.id ?? record?.barcode ?? record?.code ?? `row-${index}`
);
const PrintReportContent = ({
    contentRef,
    rows,
    reportTitle,
    printMeta,
    translatedColumns,
    printLayoutConfig,
    sequenceColumnWidth,
    dataColumnWidth,
    previewNotice,
}) => (
    <Box
        ref={contentRef}
        sx={{
            width: '277mm',
            minHeight: '186mm',
            mx: 'auto',
            px: 1.5,
            py: 2,
            backgroundColor: '#fff',
            color: '#111827',
            boxSizing: 'border-box',
        }}
    >
        <Typography
            variant="h5"
            sx={{
                mb: 1,
                textAlign: 'center',
                fontWeight: 700,
                letterSpacing: '0.08em',
            }}
        >
            {reportTitle}
        </Typography>
        <Box
            sx={{
                mb: 1.5,
                borderBottom: '2px solid #111827',
            }}
        />
        <Box
            sx={{
                display: 'flex',
                justifyContent: 'space-between',
                gap: 2,
                flexWrap: 'wrap',
                mb: 2,
                fontSize: 12,
                color: '#374151',
            }}
        >
            <Typography variant="body2">报表日期: {printMeta.reportDate}</Typography>
            <Typography variant="body2">打印人: {printMeta.operator}</Typography>
            <Typography variant="body2">打印时间: {printMeta.printedAt}</Typography>
            <Typography variant="body2">记录数: {printMeta.count}</Typography>
        </Box>
        {previewNotice && (
            <Typography variant="body2" sx={{ mb: 1.5, color: '#6b7280' }}>
                {previewNotice}
            </Typography>
        )}
        <TableContainer
            component={Paper}
            sx={{
                boxShadow: 'none',
                border: 'none',
                backgroundColor: 'transparent',
            }}
        >
            <Table
                size="small"
                sx={{
                    width: '100%',
                    tableLayout: 'fixed',
                    '& .MuiTableCell-root': {
                        borderBottom: 'none',
                        fontSize: printLayoutConfig.bodyFontSize,
                        px: printLayoutConfig.cellPaddingX,
                        py: printLayoutConfig.cellPaddingY,
                        whiteSpace: 'normal',
                        wordBreak: 'break-all',
                        overflowWrap: 'anywhere',
                        lineHeight: 1.35,
                        verticalAlign: 'top',
                        boxSizing: 'border-box',
                    },
                    '& .MuiTableHead-root .MuiTableCell-root': {
                        backgroundColor: '#f9fafb',
                        fontWeight: 700,
                        fontSize: printLayoutConfig.headerFontSize,
                        textAlign: 'center',
                    },
                    '& .MuiTableRow-root': {
                        pageBreakInside: 'avoid',
                        breakInside: 'avoid',
                    },
                }}
            >
                <colgroup>
                    <col style={{ width: sequenceColumnWidth }} />
                    {translatedColumns.map((column) => (
                        <col key={column.source} style={{ width: dataColumnWidth }} />
                    ))}
                </colgroup>
                <TableHead>
                    <TableRow>
                        <TableCell align="center">
                            序号
                        </TableCell>
                        {translatedColumns.map((column) => (
                            <TableCell key={column.source}>
                                {column.labelText}
                            </TableCell>
                        ))}
                    </TableRow>
                </TableHead>
                <TableBody>
                    {rows.map((record, index) => (
                        <TableRow key={getRecordKey(record, index)}>
                            <TableCell align="center">
                                {padNumber(index + 1)}
                            </TableCell>
                            {translatedColumns.map((column) => (
                                <TableCell key={column.source}>
                                    {formatValue(getValueBySource(record, column.source), column.fieldType)}
                                </TableCell>
                            ))}
                        </TableRow>
                    ))}
                </TableBody>
            </Table>
        </TableContainer>
    </Box>
);
const ListExportPrintButton = ({
    storeKey,
    columns = [],
    title,
    fileName,
    maxResults = 1000,
}) => {
    const {
        filter,
        selectedIds = [],
        filterValues = {},
        resource,
        sort,
        total = 0,
    } = useListContext();
    const [hiddenColumns] = useStore(
        storeKey,
        columns.filter((column) => column.hidden).map((column) => column.source)
    );
    const [columnRanks] = useStore(`${storeKey}_columnRanks`, []);
    const dataProvider = useDataProvider();
    const notify = useNotify();
    const translate = useTranslate();
    const unselectAll = useUnselectAll(resource);
    const previewContentRef = useRef(null);
    const fullPrintContentRef = useRef(null);
    const [printOpen, setPrintOpen] = useState(false);
    const [printLoading, setPrintLoading] = useState(false);
    const [printRows, setPrintRows] = useState([]);
    const [renderFullPrintContent, setRenderFullPrintContent] = useState(false);
    const defaultHiddenColumns = useMemo(
        () => columns.filter((column) => column.hidden).map((column) => column.source),
        [columns]
    );
    const visibleColumns = useMemo(
        () => resolveVisibleColumns(storeKey, defaultHiddenColumns, columns, hiddenColumns, columnRanks),
        [storeKey, defaultHiddenColumns, columns, hiddenColumns, columnRanks]
    );
    const mergedFilter = useMemo(
        () => (filter ? { ...filterValues, ...filter } : filterValues),
        [filter, filterValues]
    );
    const translatedTitle = useMemo(
        () => formatColumnLabel(title || resource, translate),
        [title, resource, translate]
    );
    const reportTitle = useMemo(() => `${translatedTitle}报表`, [translatedTitle]);
    const [printMeta, setPrintMeta] = useState(() => buildReportMeta(reportTitle, 0));
    const translatedColumns = useMemo(
        () => visibleColumns.map((column) => ({
            ...column,
            labelText: formatColumnLabel(column.label, translate),
        })),
        [visibleColumns, translate]
    );
    const previewRows = useMemo(
        () => printRows.slice(0, PREVIEW_ROW_LIMIT),
        [printRows]
    );
    const previewNotice = printRows.length > PREVIEW_ROW_LIMIT
        ? `当前仅预览前 ${PREVIEW_ROW_LIMIT} 条,打印将包含全部 ${printRows.length} 条记录`
        : '';
    const printLayoutConfig = useMemo(
        () => getPrintLayoutConfig(translatedColumns.length),
        [translatedColumns.length]
    );
    const sequenceColumnWidth = `${printLayoutConfig.sequenceWidth}px`;
    const dataColumnWidth = translatedColumns.length > 0
        ? `calc((100% - ${sequenceColumnWidth}) / ${translatedColumns.length})`
        : 'auto';
    const isDisabled = total === 0 || translatedColumns.length === 0;
    const getPerPage = () => {
        if (selectedIds.length > 0) {
            return Math.min(selectedIds.length, maxResults);
        }
        if (total > 0) {
            return Math.min(total, maxResults);
        }
        return maxResults;
    };
    const downloadBlob = (response) => {
        const url = window.URL.createObjectURL(
            new Blob([response.data], { type: response.headers["content-type"] }),
        );
        const link = document.createElement("a");
        link.href = url;
        link.setAttribute("download", `${fileName || resource}.xlsx`);
        document.body.appendChild(link);
        link.click();
        link.remove();
        window.URL.revokeObjectURL(url);
    };
    const handleExport = (event) => {
        if (translatedColumns.length === 0) {
            notify('暂无可导出的列', { type: 'warning' });
            return;
        }
        dataProvider
            .export(resource, {
                sort,
                ids: selectedIds,
                filter: mergedFilter,
                pagination: { page: 1, perPage: getPerPage() },
                meta: {
                    ...buildReportMeta(reportTitle, selectedIds.length > 0 ? selectedIds.length : total),
                    columns: translatedColumns.map((column) => ({
                        source: column.source,
                        label: column.labelText,
                    })),
                },
            })
            .then((response) => {
                downloadBlob(response);
                unselectAll();
            })
            .catch((error) => {
                console.error(error);
                notify("ra.notification.http_error", { type: "error" });
            });
        if (typeof event?.stopPropagation === 'function') {
            event.stopPropagation();
        }
    };
    const loadPrintRows = async () => {
        if (translatedColumns.length === 0) {
            notify('暂无可打印的列', { type: 'warning' });
            return;
        }
        setPrintOpen(true);
        setPrintLoading(true);
        try {
            let rows = [];
            if (selectedIds.length > 0) {
                const response = await dataProvider.getMany(resource, { ids: selectedIds });
                rows = response.data || [];
            } else {
                const response = await dataProvider.getList(resource, {
                    sort,
                    filter: mergedFilter,
                    pagination: { page: 1, perPage: getPerPage() },
                });
                rows = response.data || [];
            }
            setPrintRows(rows);
            setPrintMeta(buildReportMeta(reportTitle, rows.length));
        } catch (error) {
            console.error(error);
            notify("ra.notification.http_error", { type: "error" });
            setPrintOpen(false);
        } finally {
            setPrintLoading(false);
        }
    };
    const handlePrintDialogClose = () => {
        setPrintOpen(false);
        setPrintRows([]);
    };
    const triggerPrint = useReactToPrint({
        content: () => fullPrintContentRef.current,
        documentTitle: translatedTitle,
        onAfterPrint: () => {
            setRenderFullPrintContent(false);
        },
        pageStyle: `
            @page {
                size: A4 landscape;
                margin: 12mm 10mm;
            }
            html, body {
                width: 297mm;
                min-height: 210mm;
            }
            body {
                -webkit-print-color-adjust: exact;
                print-color-adjust: exact;
                margin: 0;
            }
        `,
    });
    const handlePrint = async () => {
        if (printLoading || printRows.length === 0) {
            return;
        }
        setRenderFullPrintContent(true);
        await new Promise((resolve) => setTimeout(resolve, 0));
        if (typeof triggerPrint === 'function') {
            triggerPrint();
        } else {
            setRenderFullPrintContent(false);
        }
    };
    return (
        <>
            <Button
                onClick={handleExport}
                label="ra.action.export"
                disabled={isDisabled}
            >
                <DownloadIcon />
            </Button>
            <Button
                onClick={loadPrintRows}
                label="toolbar.print"
                disabled={isDisabled}
            >
                <PrintOutlinedIcon />
            </Button>
            <Dialog open={printOpen} maxWidth="xl" fullWidth onClose={handlePrintDialogClose}>
                <DialogCloseButton onClose={handlePrintDialogClose} />
                <DialogTitle>{translatedTitle}</DialogTitle>
                <DialogContent dividers>
                    {printLoading ? (
                        <Box sx={{ py: 8, display: 'flex', justifyContent: 'center' }}>
                            <CircularProgress size={28} />
                        </Box>
                    ) : printRows.length === 0 ? (
                        <Box sx={{ py: 8, textAlign: 'center' }}>
                            <Typography color="text.secondary">暂无可打印数据</Typography>
                        </Box>
                    ) : (
                        <PrintReportContent
                            contentRef={previewContentRef}
                            rows={previewRows}
                            reportTitle={reportTitle}
                            printMeta={printMeta}
                            translatedColumns={translatedColumns}
                            printLayoutConfig={printLayoutConfig}
                            sequenceColumnWidth={sequenceColumnWidth}
                            dataColumnWidth={dataColumnWidth}
                            previewNotice={previewNotice}
                        />
                    )}
                </DialogContent>
                <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper' }}>
                    <MuiButton
                        onClick={handlePrint}
                        disabled={printLoading || printRows.length === 0}
                        variant="contained"
                        startIcon={<SaveIcon />}
                    >
                        {translate('toolbar.confirm')}
                    </MuiButton>
                </DialogActions>
            </Dialog>
            {renderFullPrintContent && (
                <Box sx={{ position: 'fixed', left: '-10000px', top: 0, zIndex: -1 }}>
                    <PrintReportContent
                        contentRef={fullPrintContentRef}
                        rows={printRows}
                        reportTitle={reportTitle}
                        printMeta={printMeta}
                        translatedColumns={translatedColumns}
                        printLayoutConfig={printLayoutConfig}
                        sequenceColumnWidth={sequenceColumnWidth}
                        dataColumnWidth={dataColumnWidth}
                    />
                </Box>
            )}
        </>
    );
};
export default ListExportPrintButton;
rsf-admin/src/page/components/listExportPrintUtils.js
New file
@@ -0,0 +1,62 @@
const EXTEND_FIELD_SOURCE_REGEXP = /^extendFields\.\[(.+)\]$/;
const getExtendFieldKey = (source) => {
    const match = EXTEND_FIELD_SOURCE_REGEXP.exec(source || '');
    return match ? match[1] : null;
};
const reorderColumns = (columns = [], columnRanks = []) => {
    if (!Array.isArray(columnRanks) || columnRanks.length === 0) {
        return columns;
    }
    return columns.reduce((accumulator, column, index) => {
        const rank = columnRanks.indexOf(index);
        if (rank === -1) {
            accumulator[index] = column;
        } else {
            accumulator[rank] = column;
        }
        return accumulator;
    }, []).filter(Boolean);
};
export const resolveVisibleColumns = (
    storeKey,
    defaultHiddenColumns = [],
    columns = [],
    hiddenColumns = defaultHiddenColumns,
    columnRanks = []
) => {
    if (!storeKey || !Array.isArray(columns)) {
        return [];
    }
    const visibleColumns = reorderColumns(columns, columnRanks);
    const effectiveHiddenColumns = Array.isArray(hiddenColumns) ? hiddenColumns : defaultHiddenColumns;
    return visibleColumns.filter((column) => {
        if (!column?.source) {
            return false;
        }
        return !effectiveHiddenColumns.includes(column.source);
    });
};
export const getValueBySource = (record, source) => {
    if (!record || !source) {
        return '';
    }
    const extendFieldKey = getExtendFieldKey(source);
    if (extendFieldKey) {
        return record.extendFields?.[extendFieldKey] ?? '';
    }
    return source.split('.').reduce((value, key) => {
        if (value == null) {
            return undefined;
        }
        return value[key];
    }, record) ?? '';
};
rsf-admin/src/page/warehouseAreas/WarehouseAreasList.jsx
@@ -1,75 +1,42 @@
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { useNavigate } from 'react-router-dom';
import React, { useState } from "react";
import {
    List,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    ColumnsButton,
    EditButton,
    FilterButton,
    CreateButton,
    ExportButton,
    BulkDeleteButton,
    WrapperField,
    useRecordContext,
    useTranslate,
    useNotify,
    useListContext,
    FunctionField,
    TextField,
    NumberField,
    DateField,
    BooleanField,
    ReferenceField,
    TextInput,
    DateTimeInput,
    DateInput,
    SelectInput,
    NumberInput,
    ReferenceInput,
    ReferenceArrayInput,
    AutocompleteInput,
    DeleteButton,
    useRefresh,
    Button
} from 'react-admin';
import { Box, Typography, Card, Stack } from '@mui/material';
import { styled } from '@mui/material/styles';
import { Box } from '@mui/material';
import WarehouseAreasCreate from "./WarehouseAreasCreate";
import WarehouseAreasPanel from "./WarehouseAreasPanel";
import EmptyData from "../components/EmptyData";
import MyCreateButton from "../components/MyCreateButton";
import MyExportButton from '../components/MyExportButton';
import PageDrawer from "../components/PageDrawer";
import BatchModal from "./BatchModal";
import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import * as Common from '@/utils/common';
import EditIcon from '@mui/icons-material/Edit';
import StickyDataTable from "@/page/components/StickyDataTable";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
        height: '.9em'
    },
    '& .RaDatagrid-row': {
        cursor: 'auto'
    },
    '& .column-name': {
    },
    '& .opt': {
        width: 200
    },
    '& .MuiTableCell-root': {
        whiteSpace: 'nowrap',
        overflow: 'visible',
        textOverflow: 'unset'
    }
}));
import ListExportPrintButton from "../components/ListExportPrintButton";
const filters = [
    <SearchInput source="condition" placeholder="搜索库区名称" alwaysOn />,
    <SearchInput key="condition" source="condition" placeholder="搜索库区名称" alwaysOn />,
    <ReferenceInput
        key="warehouseId"
        source="warehouseId"
        label="table.field.loc.warehouseId"
        reference="warehouse"
@@ -80,45 +47,140 @@
            filterToQuery={(val) => ({ name: val })}
        />
    </ReferenceInput>,
    // <TextInput source="uuid" label="table.field.warehouseAreas.uuid" />,
    <TextInput source="code" label="table.field.warehouseAreas.code" />,
    <TextInput source="name" label="table.field.warehouseAreas.name" />,
    <ReferenceInput source="shipperId" label="table.field.warehouseAreas.shipperId" reference="shipper">
        <AutocompleteInput label="table.field.warehouseAreas.shipperId" optionText="name" filterToQuery={(val) => ({ name: val })} />
    <TextInput key="code" source="code" label="table.field.warehouseAreas.code" />,
    <TextInput key="name" source="name" label="table.field.warehouseAreas.name" />,
    <ReferenceInput
        key="shipperId"
        source="shipperId"
        label="table.field.warehouseAreas.shipperId"
        reference="shipper"
    >
        <AutocompleteInput
            label="table.field.warehouseAreas.shipperId"
            optionText="name"
            filterToQuery={(val) => ({ name: val })}
        />
    </ReferenceInput>,
    <NumberInput source="supplierId" label="table.field.warehouseAreas.supplierId" />,
    // <SelectInput source="flagMinus" label="table.field.warehouseAreas.flagMinus"
    //     choices={[
    //         { id: 0, name: '否' },
    //         { id: 1, name: '是' },
    //     ]}
    // />,
    // <SelectInput source="flagLabelMange" label="table.field.warehouseAreas.flagLabelMange"
    //     choices={[
    //         { id: 0, name: ' 否' },
    //         { id: 1, name: ' 是' },
    //     ]}
    // />,
    // <SelectInput source="flagMix" label="table.field.warehouseAreas.flagMix"
    //     choices={[
    //         { id: 0, name: '否' },
    //         { id: 1, name: '是' },
    //     ]}
    // />,
    // <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
    // />,
]
    <NumberInput key="supplierId" source="supplierId" label="table.field.warehouseAreas.supplierId" />,
];
const HIDDEN_COLUMN_SOURCES = [
    'id',
    'updateTime',
    'updateBy',
    'createTime',
    'createBy',
    'longitude',
    'latgitude',
    'length',
    'width',
    'height',
    'shipperId$',
    'supplierId',
    'sort',
];
const createColumnDef = (source, label, fieldType = 'text', extra = {}) => ({
    source,
    label,
    fieldType,
    hidden: HIDDEN_COLUMN_SOURCES.includes(source),
    ...extra,
});
const COLUMN_DEFS = [
    createColumnDef('id', 'id', 'number'),
    createColumnDef('sort', 'table.field.warehouseAreas.sort', 'number'),
    createColumnDef('warehouseId$', 'table.field.warehouseAreas.wareId'),
    createColumnDef('code', 'table.field.warehouseAreas.code'),
    createColumnDef('name', 'table.field.warehouseAreas.name'),
    createColumnDef('type$', 'table.field.warehouseAreas.type'),
    createColumnDef('shipperId$', 'table.field.warehouseAreas.shipperId'),
    createColumnDef('supplierId', 'table.field.warehouseAreas.supplierId', 'number'),
    createColumnDef('flagMix$', 'table.field.warehouseAreas.flagMix'),
    createColumnDef('flagMinus$', 'table.field.warehouseAreas.flagMinus'),
    createColumnDef('updateBy', 'common.field.updateBy', 'text', {
        component: 'reference',
        reference: 'user',
        childSource: 'nickname',
        sortable: false,
    }),
    createColumnDef('updateTime', 'common.field.updateTime', 'date', {
        component: 'date',
        showTime: true,
    }),
    createColumnDef('createBy', 'common.field.createBy', 'text', {
        component: 'reference',
        reference: 'user',
        childSource: 'nickname',
        sortable: false,
    }),
    createColumnDef('createTime', 'common.field.createTime', 'date', {
        component: 'date',
        showTime: true,
    }),
    createColumnDef('memo', 'common.field.memo', 'text', {
        sortable: false,
    }),
];
const renderColumnField = (column) => {
    if (column.component === 'reference') {
        return (
            <ReferenceField
                key={column.source}
                source={column.source}
                label={column.label}
                reference={column.reference}
                link={false}
                sortable={column.sortable}
            >
                <TextField source={column.childSource} />
            </ReferenceField>
        );
    }
    if (column.component === 'date' || column.fieldType === 'date') {
        return (
            <DateField
                key={column.source}
                source={column.source}
                label={column.label}
                showTime={column.showTime}
            />
        );
    }
    if (column.fieldType === 'number') {
        return (
            <NumberField
                key={column.source}
                source={column.source}
                label={column.label}
            />
        );
    }
    return (
        <TextField
            key={column.source}
            source={column.source}
            label={column.label}
            sortable={column.sortable}
        />
    );
};
const exportPrintButton = (
    <ListExportPrintButton
        storeKey="warehouseAreas"
        columns={COLUMN_DEFS}
        title="menu.warehouseAreas"
        fileName="warehouseAreas"
    />
);
const WarehouseAreasList = () => {
    const translate = useTranslate();
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
@@ -133,145 +195,104 @@
                        }),
                    marginRight: drawerVal ? `${PAGE_DRAWER_WIDTH}px` : 0,
                }}
                title={"menu.warehouseAreas"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
                title="menu.warehouseAreas"
                empty={<EmptyData onClick={() => { setCreateDialog(true); }} />}
                filters={filters}
                sort={{ field: "warehouseId", order: "desc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <ColumnsButton storeKey='warehouseAreas' />
                        <MyExportButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true); }} />
                        <ColumnsButton storeKey="warehouseAreas" />
                        {exportPrintButton}
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
            >
                <StickyDataTable
                    storeKey='warehouseAreas'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={(id, resource, record) => false}
                    storeKey="warehouseAreas"
                    bulkActionButtons={
                        <>
                            {exportPrintButton}
                            <BulkDeleteButton mutationMode={OPERATE_MODE} />
                        </>
                    }
                    rowClick={() => false}
                    stickyRight={['opt']}
                    hiddenColumns={['id', 'updateTime', 'updateBy', 'createTime', 'createBy', 'longitude', 'latgitude', 'length', 'width', 'height', 'shipperId$', 'supplierId', 'sort']}
                    hiddenColumns={HIDDEN_COLUMN_SOURCES}
                >
                    <NumberField source="id" />
                    <NumberField source="sort" label="table.field.warehouseAreas.sort" />
                    <TextField source="warehouseId$" label="table.field.warehouseAreas.wareId" />
                    <TextField source="code" label="table.field.warehouseAreas.code" />
                    <TextField source="name" label="table.field.warehouseAreas.name" />
                    <TextField source="type$" label="table.field.warehouseAreas.type"/>
                    <TextField source="shipperId$" label="table.field.warehouseAreas.shipperId" />
                    <NumberField source="supplierId" label="table.field.warehouseAreas.supplierId" />
                    <TextField source="flagMix$" label="table.field.warehouseAreas.flagMix" sortable={false} />
                    <TextField source="flagMinus$" label="table.field.warehouseAreas.flagMinus" sortable={false} />
                    <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}>
                        <TextField source="nickname" />
                    </ReferenceField>
                    <DateField source="updateTime" label="common.field.updateTime" showTime />
                    <ReferenceField source="createBy" label="common.field.createBy" reference="user" link={false} sortable={false}>
                        <TextField source="nickname" />
                    </ReferenceField>
                    <DateField source="createTime" label="common.field.createTime" showTime />
                    {/* <BooleanField source="statusBool" label="common.field.status" sortable={false} /> */}
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                    {COLUMN_DEFS.map((column) => renderColumnField(column))}
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
                    {/* <TextField source="flagLabelMange$" label="table.field.warehouseAreas.flagLabelMange" sortable={false} /> */}
                    {/* <TextField source="uuid" label="table.field.warehouseAreas.uuid" /> */}
                    {/* <ReferenceField source="shipperId" label="table.field.warehouseAreas.shipperId" reference="shipper" link={false} sortable={false}>
                        <TextField source="name" />
                    </ReferenceField> */}
                </StickyDataTable>
            </List>
            <WarehouseAreasCreate
                open={createDialog}
                setOpen={setCreateDialog}
            />
            <PageDrawer
                title='WarehouseAreas Detail'
                title="WarehouseAreas Detail"
                drawerVal={drawerVal}
                setDrawerVal={setDrawerVal}
            >
            </PageDrawer>
            />
        </Box>
    )
}
    );
};
export default WarehouseAreasList;
const MixButton = () => {
    const record = useRecordContext();
    const notify = useNotify();
    const refresh = useRefresh();
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <>
            <Button onClick={() => setCreateDialog(true)} label={"toolbar.batchMix"}>
            <Button onClick={() => setCreateDialog(true)} label="toolbar.batchMix">
                <EditIcon />
            </Button>
            <BatchModal
                open={createDialog}
                setOpen={setCreateDialog}
                fieldType={'flagMix'}
                fieldType="flagMix"
            />
        </>
    )
}
    );
};
const WareButton = () => {
    const record = useRecordContext();
    const notify = useNotify();
    const refresh = useRefresh();
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <>
            <Button onClick={() => setCreateDialog(true)} label={"toolbar.batchWarehouse"}>
            <Button onClick={() => setCreateDialog(true)} label="toolbar.batchWarehouse">
                <EditIcon />
            </Button>
            <BatchModal
                open={createDialog}
                setOpen={setCreateDialog}
                fieldType={'wareId'}
                fieldType="wareId"
            />
        </>
    )
}
    );
};
const StatusButton = () => {
    const record = useRecordContext();
    const notify = useNotify();
    const refresh = useRefresh();
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <>
            <Button onClick={() => setCreateDialog(true)} label={"toolbar.batchStatus"}>
            <Button onClick={() => setCreateDialog(true)} label="toolbar.batchStatus">
                <EditIcon />
            </Button>
            <BatchModal
                open={createDialog}
                setOpen={setCreateDialog}
                fieldType={'status'}
                fieldType="status"
            />
        </>
    )
}
    );
};
rsf-admin/src/page/warehouseAreasItem/WarehouseAreasItemList.jsx
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import {
    List,
    SearchInput,
@@ -20,11 +20,11 @@
import { Box, LinearProgress } from '@mui/material';
import WarehouseAreasItemCreate from "./WarehouseAreasItemCreate";
import request from '@/utils/request';
import MyExportButton from '../components/MyExportButton';
import { DEFAULT_PAGE_SIZE, PAGE_DRAWER_WIDTH } from '@/config/setting';
import StickyDataTable from "../components/StickyDataTable";
import WarehouseIsptResult from "./WarehouseIsptResult";
import useTableLayout from '@/utils/useTableLayout';
import ListExportPrintButton from "../components/ListExportPrintButton";
const baseFilters = [
    <SearchInput key="condition" source="condition" alwaysOn />,
@@ -50,7 +50,6 @@
    <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"
@@ -66,61 +65,132 @@
const baseFilterSources = new Set(baseFilters.map((item) => item.props.source).filter(Boolean));
const hiddenColumns = [
const hiddenColumnSources = [
    'prodTime', 'platOrderCode', 'id', 'createTime', 'memo', 'areaId', 'brand',
    'weight', 'splrId', 'projectCode', 'statusBool', 'extendFields.[priceUnitId]', 'isptResult$',
    'extendFields.[inStockType]', 'matnrId', 'trackCode', 'workQty', 'batch', 'shipperId',
    'isptResult', 'createBy$', 'extendFields.[baseUnitId]'
];
const baseColumns = [
    <NumberField key="id" source="id" />,
    <TextField key="areaName" source="areaName" label="收货区名称" />,
    <TextField key="asnCode" source="asnCode" label="table.field.warehouseAreasItem.asnCode" />,
    <TextField key="platWorkCode" source="platWorkCode" label="table.field.asnOrderItem.platWorkCode" />,
    <TextField key="platItemId" 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 key="platOrderCode" source="platOrderCode" label="table.field.asnOrderItem.platOrderCode" />,
    <TextField key="projectCode" source="projectCode" label="table.field.asnOrderItem.projectCode" />,
    <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 createColumnDef = (source, label, fieldType = 'text', options = {}) => ({
    source,
    label,
    fieldType,
    hidden: hiddenColumnSources.includes(source),
    sortable: options.sortable,
});
const baseColumnDefs = [
    createColumnDef('id', 'table.field.warehouseAreasItem.id', 'number'),
    createColumnDef('areaName', 'table.field.warehouseAreasItem.areaName'),
    createColumnDef('asnCode', 'table.field.warehouseAreasItem.asnCode'),
    createColumnDef('platWorkCode', 'table.field.asnOrderItem.platWorkCode'),
    createColumnDef('platItemId', 'table.field.deliveryItem.platItemId'),
    createColumnDef('matnrId', 'table.field.warehouseAreasItem.matnrId', 'number'),
    createColumnDef('matnrCode', 'table.field.warehouseAreasItem.matnrCode'),
    createColumnDef('maktx', 'table.field.warehouseAreasItem.matnrName'),
    createColumnDef('splrBatch', 'table.field.warehouseAreasItem.splrBtch'),
    createColumnDef('batch', 'table.field.warehouseAreasItem.batch'),
    createColumnDef('trackCode', 'table.field.warehouseAreasItem.barcode'),
    createColumnDef('unit', 'table.field.warehouseAreasItem.unit'),
    createColumnDef('anfme', 'table.field.warehouseAreasItem.anfme', 'number'),
    createColumnDef('workQty', 'table.field.warehouseAreasItem.workQty', 'number'),
    createColumnDef('ableQty', 'table.field.warehouseAreasItem.qty', 'number'),
    createColumnDef('platOrderCode', 'table.field.asnOrderItem.platOrderCode'),
    createColumnDef('projectCode', 'table.field.asnOrderItem.projectCode'),
    createColumnDef('brand', 'table.field.warehouseAreasItem.brand'),
    createColumnDef('shipperId', 'table.field.warehouseAreasItem.shipperId'),
    createColumnDef('splrId', 'table.field.warehouseAreasItem.splrId'),
    createColumnDef('isptResult$', 'table.field.warehouseAreasItem.isptResult', 'text', { sortable: false }),
    createColumnDef('weight', 'table.field.warehouseAreasItem.weight', 'number'),
    createColumnDef('prodTime', 'table.field.warehouseAreasItem.prodTime'),
];
const trailingColumns = [
    <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} />,
const trailingColumnDefs = [
    createColumnDef('updateBy$', 'common.field.updateBy'),
    createColumnDef('updateTime', 'common.field.updateTime', 'date'),
    createColumnDef('createBy$', 'common.field.createBy'),
    createColumnDef('createTime', 'common.field.createTime', 'date'),
    createColumnDef('statusBool', 'common.field.status', 'boolean', { sortable: false }),
    createColumnDef('memo', 'common.field.memo', 'text', { sortable: false }),
];
const buildDynamicFilter = (field) => (
    <TextInput key={field.fields} source={field.fields} label={field.fieldsAlise} />
);
const buildDynamicColumn = (field) => (
    <TextField key={field.fields} source={`extendFields.[${field.fields}]`} label={field.fieldsAlise} />
const buildDynamicColumnDef = (field) => createColumnDef(
    `extendFields.[${field.fields}]`,
    field.fieldsAlise
);
const buildFieldElement = (column) => {
    const commonProps = {
        key: column.source,
        source: column.source,
        label: column.label,
    };
    if (column.sortable === false) {
        commonProps.sortable = false;
    }
    switch (column.fieldType) {
        case 'number':
            return <NumberField {...commonProps} />;
        case 'date':
            return <DateField {...commonProps} showTime />;
        case 'boolean':
            return <BooleanField {...commonProps} />;
        default:
            return <TextField {...commonProps} />;
    }
};
const WarehouseAreasItemList = () => {
    const [itemInfo, setItemInfo] = useState({})
    const notify = useNotify();
    const [itemInfo, setItemInfo] = useState({});
    const [createDialog, setCreateDialog] = useState(false);
    const [drawerVal, setDrawerVal] = useState(false);
    const [dynamicFilters, setDynamicFilters] = useState([]);
    const [columnDefs, setColumnDefs] = useState([]);
    useEffect(() => {
        let active = true;
        const getDynamicFields = async () => {
            try {
                const { data: { code, data, msg } } = await request.get("/fields/enable/list");
                if (!active) {
                    return;
                }
                if (code === 200) {
                    const dynamicColumnDefs = data.map(buildDynamicColumnDef);
                    const nextDynamicFilters = data
                        .filter((field) => !baseFilterSources.has(field.fields))
                        .map(buildDynamicFilter);
                    setColumnDefs([...baseColumnDefs, ...dynamicColumnDefs, ...trailingColumnDefs]);
                    setDynamicFilters(nextDynamicFilters);
                } else {
                    setColumnDefs([...baseColumnDefs, ...trailingColumnDefs]);
                    notify(msg);
                }
            } catch (error) {
                if (active) {
                    setColumnDefs([...baseColumnDefs, ...trailingColumnDefs]);
                    notify('请求出错');
                }
            }
        };
        getDynamicFields();
        return () => {
            active = false;
            setDynamicFilters([]);
        };
    }, [notify]);
    return (
        <Box display="flex">
@@ -139,8 +209,16 @@
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <ColumnsButton storeKey='warehouseAreasItem' />
                        <MyExportButton />
                        {columnDefs.length > 0 && (
                            <ColumnsButton storeKey='warehouseAreasItem' />
                        )}
                        {columnDefs.length > 0 && (
                            <ListExportPrintButton
                                storeKey='warehouseAreasItem'
                                columns={columnDefs}
                                title='menu.warehouseAreasItem'
                            />
                        )}
                    </TopToolbar>
                )}
                filters={[...baseFilters, ...dynamicFilters]}
@@ -148,7 +226,7 @@
            >
                <DynamicFields
                    drawerOpen={!!drawerVal}
                    onDynamicFiltersChange={setDynamicFilters}
                    columnDefs={columnDefs}
                />
            </List>
            <WarehouseAreasItemCreate
@@ -162,12 +240,6 @@
                setDrawerVal={setDrawerVal}
            >
            </WarehouseIsptResult>
            {/* <PageDrawer
                title='WarehouseAreasItem Detail'
                drawerVal={drawerVal}
                setDrawerVal={setDrawerVal}
            >
            </PageDrawer> */}
        </Box>
    )
}
@@ -175,46 +247,10 @@
export default WarehouseAreasItemList;
const DynamicFields = ({ drawerOpen, onDynamicFiltersChange }) => {
    const notify = useNotify();
    const [columns, setColumns] = useState([]);
const DynamicFields = ({ drawerOpen, columnDefs }) => {
    const { isLoading } = useListContext();
    const { boxMaxWidth, boxMaxHeight } = useTableLayout(drawerOpen);
    useEffect(() => {
        let active = true;
        const getDynamicFields = async () => {
            try {
                const { data: { code, data, msg }, } = await request.get("/fields/enable/list");
                if (!active) {
                    return;
                }
                if (code === 200) {
                    const dynamicColumns = data.map(buildDynamicColumn);
                    const nextDynamicFilters = data
                        .filter((field) => !baseFilterSources.has(field.fields))
                        .map(buildDynamicFilter);
                    setColumns([...baseColumns, ...dynamicColumns, ...trailingColumns]);
                    onDynamicFiltersChange(nextDynamicFilters);
                } else {
                    notify(msg);
                }
            } catch (error) {
                if (active) {
                    notify('请求出错');
                }
            }
        };
        getDynamicFields();
        return () => {
            active = false;
            onDynamicFiltersChange([]);
        };
    }, [notify, onDynamicFiltersChange]);
    const columns = columnDefs.map(buildFieldElement);
    return (
        <Box sx={{
@@ -241,9 +277,15 @@
            {columns.length > 0 &&
                <StickyDataTable
                    storeKey='warehouseAreasItem'
                    bulkActionButtons={false}
                    bulkActionButtons={(
                        <ListExportPrintButton
                            storeKey='warehouseAreasItem'
                            columns={columnDefs}
                            title='menu.warehouseAreasItem'
                        />
                    )}
                    rowClick={false}
                    hiddenColumns={hiddenColumns}
                    hiddenColumns={hiddenColumnSources}
                >
                    {columns}
                </StickyDataTable>}
rsf-server/src/main/java/com/vincent/rsf/server/common/service/ListExportHandler.java
New file
@@ -0,0 +1,20 @@
package com.vincent.rsf.server.common.service;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import java.util.List;
import java.util.Map;
public interface ListExportHandler<T, P extends BaseParam> {
    List<T> listByIds(List<Long> ids);
    List<T> listByFilter(Map<String, Object> sanitizedMap, P baseParam);
    default void fillExportFields(List<T> records) {
    }
    Map<String, Object> toExportRow(T record, List<ExcelUtil.ExportColumn> columns);
    String defaultReportTitle();
}
rsf-server/src/main/java/com/vincent/rsf/server/common/service/ListExportService.java
New file
@@ -0,0 +1,154 @@
package com.vincent.rsf.server.common.service;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import org.springframework.stereotype.Service;
import jakarta.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
@Service
public class ListExportService {
    private static final Set<String> EXPORT_ONLY_KEYS = Set.of(
            "columns",
            "reportTitle",
            "reportDate",
            "printedAt",
            "operator",
            "count",
            "report_title",
            "report_date",
            "printed_at"
    );
    public <T, P extends BaseParam> void export(
            Map<String, Object> map,
            Function<Map<String, Object>, P> paramBuilder,
            ListExportHandler<T, P> exportHandler,
            HttpServletResponse response
    ) throws Exception {
        Map<String, Object> sanitizedMap = sanitizeExportMap(map);
        P baseParam = paramBuilder.apply(sanitizedMap);
        List<ExcelUtil.ExportColumn> columns = buildExportColumns(map);
        if (columns.isEmpty()) {
            throw new CoolException("导出列不能为空");
        }
        List<T> records = getExportRecords(sanitizedMap, baseParam, exportHandler);
        exportHandler.fillExportFields(records);
        List<Map<String, Object>> rows = records.stream()
                .map(record -> exportHandler.toExportRow(record, columns))
                .toList();
        ExcelUtil.ExportMeta exportMeta = buildExportMeta(map, rows.size(), exportHandler.defaultReportTitle());
        ExcelUtil.build(ExcelUtil.create(rows, columns, exportMeta), response);
    }
    private Map<String, Object> sanitizeExportMap(Map<String, Object> map) {
        Map<String, Object> exportMap = new HashMap<>(map);
        EXPORT_ONLY_KEYS.forEach(exportMap::remove);
        exportMap.remove("meta");
        sanitizeNestedMap(exportMap, "filter");
        sanitizeNestedMap(exportMap, "filterValues");
        return exportMap;
    }
    private void sanitizeNestedMap(Map<String, Object> exportMap, String key) {
        Object nestedObject = exportMap.get(key);
        if (nestedObject instanceof Map<?, ?> nestedMap) {
            Map<String, Object> sanitizedMap = new HashMap<>();
            for (Map.Entry<?, ?> entry : nestedMap.entrySet()) {
                String nestedKey = String.valueOf(entry.getKey());
                if (!EXPORT_ONLY_KEYS.contains(nestedKey)) {
                    sanitizedMap.put(nestedKey, entry.getValue());
                }
            }
            exportMap.put(key, sanitizedMap);
        }
    }
    private List<ExcelUtil.ExportColumn> buildExportColumns(Map<String, Object> map) {
        Object columnsObject = map.get("columns");
        if (Objects.isNull(columnsObject)) {
            Object metaObject = map.get("meta");
            if (metaObject instanceof Map<?, ?> metaMap) {
                columnsObject = metaMap.get("columns");
            }
        }
        if (!(columnsObject instanceof List<?> rawColumns)) {
            return Collections.emptyList();
        }
        List<ExcelUtil.ExportColumn> columns = new ArrayList<>();
        for (Object rawColumn : rawColumns) {
            if (!(rawColumn instanceof Map<?, ?> columnMap)) {
                continue;
            }
            Object source = columnMap.get("source");
            Object label = columnMap.get("label");
            if (Objects.isNull(source) || Objects.isNull(label)) {
                continue;
            }
            columns.add(new ExcelUtil.ExportColumn(String.valueOf(source), String.valueOf(label)));
        }
        return columns;
    }
    private <T, P extends BaseParam> List<T> getExportRecords(
            Map<String, Object> sanitizedMap,
            P baseParam,
            ListExportHandler<T, P> exportHandler
    ) {
        List<Long> ids = parseExportIds(sanitizedMap.get("ids"));
        if (!ids.isEmpty()) {
            return exportHandler.listByIds(ids);
        }
        return exportHandler.listByFilter(sanitizedMap, baseParam);
    }
    private List<Long> parseExportIds(Object idsObject) {
        if (idsObject instanceof List<?> ids && !ids.isEmpty()) {
            return ids.stream()
                    .map(String::valueOf)
                    .map(Long::valueOf)
                    .toList();
        }
        if (idsObject instanceof Object[] ids && ids.length > 0) {
            List<Long> exportIds = new ArrayList<>();
            for (Object id : ids) {
                exportIds.add(Long.valueOf(String.valueOf(id)));
            }
            return exportIds;
        }
        return Collections.emptyList();
    }
    private ExcelUtil.ExportMeta buildExportMeta(Map<String, Object> map, int count, String defaultReportTitle) {
        Object metaObject = map.get("meta");
        if (!(metaObject instanceof Map<?, ?> metaMap)) {
            return new ExcelUtil.ExportMeta(defaultReportTitle, "", "", "", count);
        }
        String reportTitle = getMetaValue(metaMap, "reportTitle", defaultReportTitle);
        String reportDate = getMetaValue(metaMap, "reportDate", "");
        String printedAt = getMetaValue(metaMap, "printedAt", "");
        String operator = getMetaValue(metaMap, "operator", "");
        return new ExcelUtil.ExportMeta(reportTitle, reportDate, printedAt, operator, count);
    }
    private String getMetaValue(Map<?, ?> metaMap, String key, String defaultValue) {
        Object value = metaMap.get(key);
        return Objects.isNull(value) ? defaultValue : String.valueOf(value);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java
@@ -15,6 +15,7 @@
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;
@@ -23,12 +24,16 @@
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * Created by vincent on 2/17/2024
 */
@Slf4j
public class ExcelUtil {
    private static final Pattern EXTEND_FIELD_SOURCE_PATTERN = Pattern.compile("^extendFields\\.\\[(.+)]$");
    public static void build(Workbook workbook, HttpServletResponse response) {
        response.reset();
        Http.cors(response);
@@ -112,14 +117,7 @@
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    if (value != null) {
                        if (value instanceof Date) {
                            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                            row.createCell(cellIndex).setCellValue(sdf.format((Date) value));
                        } else {
                            row.createCell(cellIndex).setCellValue(value.toString());
                        }
                    }
                    writeCellValue(row, cellIndex, value);
                    cellIndex++;
                }
            }
@@ -131,6 +129,208 @@
        return workbook;
    }
    public static Workbook create(List<Map<String, Object>> rows, List<ExportColumn> columns) {
        return create(rows, columns, null);
    }
    public static Workbook create(List<Map<String, Object>> rows, List<ExportColumn> columns, ExportMeta exportMeta) {
        XSSFWorkbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("export");
        int titleColumnCount = Math.max(columns.size(), 4);
        int currentRowIndex = 0;
        CellStyle titleStyle = createTitleStyle(workbook);
        CellStyle subHeaderStyle = createSubHeaderStyle(workbook);
        CellStyle headerStyle = createHeaderStyle(workbook);
        CellStyle bodyStyle = createBodyStyle(workbook);
        if (exportMeta != null && StringUtils.isNotBlank(exportMeta.getReportTitle())) {
            Row titleRow = sheet.createRow(currentRowIndex++);
            titleRow.setHeightInPoints(24);
            Cell titleCell = titleRow.createCell(0);
            titleCell.setCellValue(exportMeta.getReportTitle());
            titleCell.setCellStyle(titleStyle);
            sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, titleColumnCount - 1));
            Row subHeaderRow = sheet.createRow(currentRowIndex++);
            writeSubHeaderCell(subHeaderRow, 0, getSubHeaderText("报表日期", exportMeta.getReportDate()), subHeaderStyle);
            writeSubHeaderCell(subHeaderRow, 1, getSubHeaderText("打印人", exportMeta.getOperator()), subHeaderStyle);
            writeSubHeaderCell(subHeaderRow, 2, getSubHeaderText("打印时间", exportMeta.getPrintedAt()), subHeaderStyle);
            writeSubHeaderCell(subHeaderRow, 3, getSubHeaderText("记录数", String.valueOf(exportMeta.getCount())), subHeaderStyle);
            currentRowIndex++;
        }
        Row header = sheet.createRow(currentRowIndex++);
        for (int index = 0; index < columns.size(); index++) {
            Cell headerCell = header.createCell(index);
            headerCell.setCellValue(columns.get(index).getLabel());
            headerCell.setCellStyle(headerStyle);
        }
        int rowIndex = currentRowIndex;
        for (Map<String, Object> rowData : rows) {
            Row row = sheet.createRow(rowIndex++);
            for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) {
                Object value = getRowValue(rowData, columns.get(columnIndex).getSource());
                writeCellValue(row, columnIndex, value, bodyStyle);
            }
        }
        for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) {
            sheet.autoSizeColumn(columnIndex);
        }
        return workbook;
    }
    private static Object getRowValue(Map<String, Object> rowData, String source) {
        if (rowData == null || StringUtils.isBlank(source)) {
            return null;
        }
        if (rowData.containsKey(source)) {
            return rowData.get(source);
        }
        Matcher matcher = EXTEND_FIELD_SOURCE_PATTERN.matcher(source);
        if (matcher.matches()) {
            Object extendFields = rowData.get("extendFields");
            if (extendFields instanceof Map<?, ?> extendFieldMap) {
                return extendFieldMap.get(matcher.group(1));
            }
        }
        return rowData.get(source);
    }
    private static void writeCellValue(Row row, int cellIndex, Object value) {
        writeCellValue(row, cellIndex, value, null);
    }
    private static void writeCellValue(Row row, int cellIndex, Object value, CellStyle cellStyle) {
        Cell cell = row.createCell(cellIndex);
        if (cellStyle != null) {
            cell.setCellStyle(cellStyle);
        }
        if (value == null) {
            return;
        }
        if (value instanceof Date) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            cell.setCellValue(sdf.format((Date) value));
            return;
        }
        cell.setCellValue(value.toString());
    }
    private static void writeSubHeaderCell(Row row, int cellIndex, String value, CellStyle cellStyle) {
        Cell cell = row.createCell(cellIndex);
        cell.setCellValue(value);
        cell.setCellStyle(cellStyle);
    }
    private static String getSubHeaderText(String label, String value) {
        return label + ": " + StringUtils.defaultString(value);
    }
    private static CellStyle createTitleStyle(Workbook workbook) {
        CellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        Font font = workbook.createFont();
        font.setBold(true);
        font.setFontHeightInPoints((short) 14);
        cellStyle.setFont(font);
        return cellStyle;
    }
    private static CellStyle createSubHeaderStyle(Workbook workbook) {
        CellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HorizontalAlignment.LEFT);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        Font font = workbook.createFont();
        font.setFontHeightInPoints((short) 10);
        cellStyle.setFont(font);
        return cellStyle;
    }
    private static CellStyle createHeaderStyle(Workbook workbook) {
        CellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        Font font = workbook.createFont();
        font.setBold(true);
        font.setFontHeightInPoints((short) 10);
        cellStyle.setFont(font);
        return cellStyle;
    }
    private static CellStyle createBodyStyle(Workbook workbook) {
        CellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HorizontalAlignment.LEFT);
        cellStyle.setVerticalAlignment(VerticalAlignment.TOP);
        cellStyle.setWrapText(true);
        return cellStyle;
    }
    public static class ExportColumn {
        private final String source;
        private final String label;
        public ExportColumn(String source, String label) {
            this.source = source;
            this.label = label;
        }
        public String getSource() {
            return source;
        }
        public String getLabel() {
            return label;
        }
    }
    public static class ExportMeta {
        private final String reportTitle;
        private final String reportDate;
        private final String printedAt;
        private final String operator;
        private final int count;
        public ExportMeta(String reportTitle, String reportDate, String printedAt, String operator, int count) {
            this.reportTitle = reportTitle;
            this.reportDate = reportDate;
            this.printedAt = printedAt;
            this.operator = operator;
            this.count = count;
        }
        public String getReportTitle() {
            return reportTitle;
        }
        public String getReportDate() {
            return reportDate;
        }
        public String getPrintedAt() {
            return printedAt;
        }
        public String getOperator() {
            return operator;
        }
        public int getCount() {
            return count;
        }
    }
    /**
     * 添加导入excel配置参数
     * 注:默认配置可满足当前需求
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasController.java
@@ -1,15 +1,18 @@
package com.vincent.rsf.server.manager.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.common.annotation.OperationLog;
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.service.ListExportHandler;
import com.vincent.rsf.server.common.service.ListExportService;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.manager.controller.params.WarehouseAreaParam;
import com.vincent.rsf.server.manager.entity.Loc;
import com.vincent.rsf.server.manager.entity.WarehouseAreas;
@@ -18,6 +21,8 @@
import com.vincent.rsf.server.system.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -34,6 +39,42 @@
    @Autowired
    private LocService locService;
    @Autowired
    private ListExportService listExportService;
    private final ListExportHandler<WarehouseAreas, BaseParam> warehouseAreasExportHandler = new ListExportHandler<>() {
        @Override
        public List<WarehouseAreas> listByIds(List<Long> ids) {
            return warehouseAreasService.listByIds(ids);
        }
        @Override
        public List<WarehouseAreas> listByFilter(Map<String, Object> sanitizedMap, BaseParam baseParam) {
            PageParam<WarehouseAreas, BaseParam> pageParam = new PageParam<>(baseParam, WarehouseAreas.class);
            QueryWrapper<WarehouseAreas> queryWrapper = pageParam.buildWrapper(true);
            return warehouseAreasService.list(queryWrapper);
        }
        @Override
        public Map<String, Object> toExportRow(WarehouseAreas record, List<ExcelUtil.ExportColumn> columns) {
            BeanWrapper beanWrapper = new BeanWrapperImpl(record);
            Map<String, Object> row = new LinkedHashMap<>();
            for (ExcelUtil.ExportColumn column : columns) {
                if (beanWrapper.isReadableProperty(column.getSource())) {
                    row.put(column.getSource(), beanWrapper.getPropertyValue(column.getSource()));
                }
            }
            return row;
        }
        @Override
        public String defaultReportTitle() {
            return "仓库库区报表";
        }
    };
    @PreAuthorize("hasAuthority('manager:warehouseAreas:list')")
    @PostMapping("/warehouseAreas/page")
@@ -170,7 +211,12 @@
    @PreAuthorize("hasAuthority('manager:warehouseAreas:list')")
    @PostMapping("/warehouseAreas/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(warehouseAreasService.list(), WarehouseAreas.class), response);
        listExportService.export(
                map,
                exportMap -> buildParam(exportMap, BaseParam.class),
                warehouseAreasExportHandler,
                response
        );
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WarehouseAreasItemController.java
@@ -6,16 +6,20 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.common.annotation.OperationLog;
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.service.ListExportHandler;
import com.vincent.rsf.server.common.service.ListExportService;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.common.utils.FieldsUtils;
import com.vincent.rsf.server.manager.entity.WarehouseAreasItem;
import com.vincent.rsf.server.manager.service.WarehouseAreasItemService;
import com.vincent.rsf.server.system.controller.BaseController;
import io.swagger.annotations.Api;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -26,9 +30,41 @@
@Api(tags = "库区库存明细")
@RestController
public class WarehouseAreasItemController extends BaseController {
    @Autowired
    private WarehouseAreasItemService warehouseAreasItemService;
    @Autowired
    private ListExportService listExportService;
    private final ListExportHandler<WarehouseAreasItem, BaseParam> warehouseAreasItemExportHandler = new ListExportHandler<>() {
        @Override
        public List<WarehouseAreasItem> listByIds(List<Long> ids) {
            return warehouseAreasItemService.listByIds(ids);
        }
        @Override
        public List<WarehouseAreasItem> listByFilter(Map<String, Object> sanitizedMap, BaseParam baseParam) {
            PageParam<WarehouseAreasItem, BaseParam> pageParam = new PageParam<>(baseParam, WarehouseAreasItem.class);
            QueryWrapper<WarehouseAreasItem> queryWrapper = pageParam.buildWrapper(true);
            FieldsUtils.setFieldsFilters(queryWrapper, pageParam, WarehouseAreasItem.class);
            return warehouseAreasItemService.list(queryWrapper);
        }
        @Override
        public void fillExportFields(List<WarehouseAreasItem> records) {
            fillExtendFields(records);
        }
        @Override
        public Map<String, Object> toExportRow(WarehouseAreasItem record, List<ExcelUtil.ExportColumn> columns) {
            return buildExportRow(record, columns);
        }
        @Override
        public String defaultReportTitle() {
            return "收货库存报表";
        }
    };
    @PreAuthorize("hasAuthority('manager:warehouseAreasItem:list')")
    @PostMapping("/warehouseAreasItem/page")
@@ -141,7 +177,37 @@
    @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);
        listExportService.export(
                map,
                exportMap -> buildParam(exportMap, BaseParam.class),
                warehouseAreasItemExportHandler,
                response
        );
    }
    private void fillExtendFields(List<WarehouseAreasItem> records) {
        for (WarehouseAreasItem record : records) {
            if (!Objects.isNull(record.getFieldsIndex())) {
                Map<String, String> fields = FieldsUtils.getFields(record.getFieldsIndex());
                record.setExtendFields(fields);
            }
        }
    }
    private Map<String, Object> buildExportRow(WarehouseAreasItem record, List<ExcelUtil.ExportColumn> columns) {
        BeanWrapper beanWrapper = new BeanWrapperImpl(record);
        Map<String, Object> row = new LinkedHashMap<>();
        row.put("extendFields", record.getExtendFields());
        for (ExcelUtil.ExportColumn column : columns) {
            if (column.getSource().startsWith("extendFields.[")) {
                continue;
            }
            if (beanWrapper.isReadableProperty(column.getSource())) {
                row.put(column.getSource(), beanWrapper.getPropertyValue(column.getSource()));
            }
        }
        return row;
    }
}