| | |
| | | |
| | | import React from 'react'; |
| | | import React, { useMemo } from 'react'; |
| | | import { DataTable, useDataTableDataContext, useTranslate } from 'react-admin'; |
| | | import { TableFooter, TableRow, TableCell } from '@mui/material'; |
| | | |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 固定列样式常量 |
| | | */ |
| | | const stickyLeftStyle = { |
| | | position: 'sticky', |
| | | left: 0, |
| | | zIndex: 2, |
| | | backgroundColor: '#FFFFFF', |
| | | '.MuiTableRow-root:not(.MuiTableRow-head):hover &': { |
| | | backgroundColor: '#f5f5f5' |
| | | } |
| | | }; |
| | | |
| | | const stickyRightStyle = { |
| | | position: 'sticky', |
| | | right: 0, |
| | | zIndex: 2, |
| | | backgroundColor: '#FFFFFF', |
| | | '.MuiTableRow-root:not(.MuiTableRow-head):hover &': { |
| | | backgroundColor: '#f5f5f5' |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * DataTable 样式常量 |
| | | */ |
| | | const tableStyles = { |
| | | '& .MuiTableCell-head': { |
| | | zIndex: 4, |
| | | borderBottom: 'none' |
| | | }, |
| | | '& .MuiTableFooter-root': { |
| | | position: 'sticky', |
| | | bottom: 0, |
| | | zIndex: 3, |
| | | backgroundColor: '#FFFFFF', |
| | | }, |
| | | '& .MuiTableFooter-root .MuiTableCell-root': { |
| | | backgroundColor: '#f5f5f5', |
| | | fontWeight: 'bold', |
| | | borderTop: '2px solid #e0e0e0', |
| | | }, |
| | | }; |
| | | |
| | | /** |
| | | * 内部 Footer 组件 |
| | | * @param {Object} props |
| | | * @param {Array} props.footerConfig - footer 配置数组 |
| | |
| | | const data = useDataTableDataContext(); |
| | | const translate = useTranslate(); |
| | | |
| | | const results = footerConfig.map(config => { |
| | | // 缓存计算结果,避免不必要的重复计算 |
| | | const results = useMemo(() => footerConfig.map(config => { |
| | | const { field, type = 'sum', label, render } = config; |
| | | const calculator = calculators[type]; |
| | | const value = calculator ? calculator(data, field) : 0; |
| | |
| | | // 获取翻译后的标签 |
| | | const displayLabel = label ? (label.startsWith('table.') || label.startsWith('common.') ? translate(label) : label) : field; |
| | | return { label: displayLabel, value, typePrefix }; |
| | | }); |
| | | }), [footerConfig, data, translate]); |
| | | |
| | | return ( |
| | | <TableFooter> |
| | |
| | | ...props |
| | | }) => { |
| | | |
| | | // 递归处理 Children,确保即便是 Fragment 包裹的列也能被处理 |
| | | const processChildren = (children) => { |
| | | return React.Children.map(children, (child) => { |
| | | if (!React.isValidElement(child)) return child; |
| | | // 使用 Set 优化查找性能 O(n) -> O(1) |
| | | const stickyLeftSet = useMemo(() => new Set(stickyLeft), [stickyLeft]); |
| | | const stickyRightSet = useMemo(() => new Set(stickyRight), [stickyRight]); |
| | | |
| | | // 如果是 Fragment,递归处理其 children |
| | | if (child.type === React.Fragment) { |
| | | return <React.Fragment>{processChildren(child.props.children)}</React.Fragment>; |
| | | } |
| | | // 缓存处理后的 children,避免不必要的重新计算 |
| | | const processedChildren = useMemo(() => { |
| | | const processChildren = (children) => { |
| | | return React.Children.map(children, (child) => { |
| | | if (!React.isValidElement(child)) return child; |
| | | |
| | | const source = child.props.source; |
| | | let stickyStyle = {}; |
| | | // 如果是 Fragment,递归处理其 children |
| | | if (child.type === React.Fragment) { |
| | | return <React.Fragment>{processChildren(child.props.children)}</React.Fragment>; |
| | | } |
| | | |
| | | // 左侧固定 |
| | | if (stickyLeft.includes(source)) { |
| | | stickyStyle = { |
| | | position: 'sticky', |
| | | left: 0, |
| | | zIndex: 2, // 比普通内容高 |
| | | backgroundColor: '#FFFFFF', |
| | | '.MuiTableRow-root:not(.MuiTableRow-head):hover &': { |
| | | backgroundColor: '#f5f5f5' |
| | | } |
| | | }; |
| | | } |
| | | const source = child.props.source; |
| | | let stickyStyle = null; |
| | | |
| | | // 右侧固定 |
| | | if (stickyRight.includes(source)) { |
| | | stickyStyle = { |
| | | position: 'sticky', |
| | | right: 0, |
| | | zIndex: 2, |
| | | backgroundColor: '#FFFFFF', |
| | | '.MuiTableRow-root:not(.MuiTableRow-head):hover &': { |
| | | backgroundColor: '#f5f5f5' |
| | | } |
| | | }; |
| | | } |
| | | // 左侧固定 |
| | | if (stickyLeftSet.has(source)) { |
| | | stickyStyle = stickyLeftStyle; |
| | | } |
| | | |
| | | if (Object.keys(stickyStyle).length > 0) { |
| | | // 合并 sx |
| | | return React.cloneElement(child, { |
| | | sx: { ...child.props.sx, ...stickyStyle } |
| | | }); |
| | | } |
| | | // 右侧固定 |
| | | if (stickyRightSet.has(source)) { |
| | | stickyStyle = stickyRightStyle; |
| | | } |
| | | |
| | | return child; |
| | | }); |
| | | }; |
| | | if (stickyStyle) { |
| | | // 合并 sx |
| | | return React.cloneElement(child, { |
| | | sx: { ...child.props.sx, ...stickyStyle } |
| | | }); |
| | | } |
| | | |
| | | // 构建 foot 属性 |
| | | const footerComponent = footerConfig && footerConfig.length > 0 |
| | | ? () => <StickyTableFooter footerConfig={footerConfig} footerLabel={footerLabel} /> |
| | | : undefined; |
| | | return child; |
| | | }); |
| | | }; |
| | | return processChildren(children); |
| | | }, [children, stickyLeftSet, stickyRightSet]); |
| | | |
| | | // 缓存 footerComponent,避免每次渲染创建新函数 |
| | | const footerComponent = useMemo(() => { |
| | | if (!footerConfig?.length) return undefined; |
| | | return () => <StickyTableFooter footerConfig={footerConfig} footerLabel={footerLabel} />; |
| | | }, [footerConfig, footerLabel]); |
| | | |
| | | return ( |
| | | <DataTable {...props} foot={footerComponent} sx={{ |
| | | '& .MuiTableCell-head': { |
| | | zIndex: 4, |
| | | borderBottom: 'none' // 遵循之前的优化,去除表头下边框 |
| | | }, |
| | | '& .MuiTableFooter-root': { |
| | | position: 'sticky', |
| | | bottom: 0, |
| | | zIndex: 3, |
| | | backgroundColor: '#FFFFFF', |
| | | }, |
| | | '& .MuiTableFooter-root .MuiTableCell-root': { |
| | | backgroundColor: '#f5f5f5', |
| | | fontWeight: 'bold', |
| | | borderTop: '2px solid #e0e0e0', |
| | | }, |
| | | }}> |
| | | {processChildren(children)} |
| | | <DataTable {...props} foot={footerComponent} sx={tableStyles}> |
| | | {processedChildren} |
| | | </DataTable> |
| | | ); |
| | | }; |