|
import React, { useMemo } from 'react';
|
import { DataTable, useDataTableDataContext, useTranslate } from 'react-admin';
|
import { TableFooter, TableRow, TableCell } from '@mui/material';
|
|
/**
|
* 计算函数映射
|
*/
|
const calculators = {
|
sum: (data, field) => data.reduce((acc, record) => acc + (Number(record[field]) || 0), 0),
|
count: (data, field) => data.filter(record => record[field] != null && record[field] !== '').length,
|
avg: (data, field) => {
|
const validData = data.filter(record => record[field] != null);
|
if (validData.length === 0) return 0;
|
const sum = validData.reduce((acc, record) => acc + (Number(record[field]) || 0), 0);
|
return (sum / validData.length).toFixed(2);
|
},
|
min: (data, field) => {
|
const values = data.map(record => Number(record[field]) || 0);
|
return values.length > 0 ? Math.min(...values) : 0;
|
},
|
max: (data, field) => {
|
const values = data.map(record => Number(record[field]) || 0);
|
return values.length > 0 ? Math.max(...values) : 0;
|
},
|
};
|
|
/**
|
* 计算类型的中文前缀映射
|
*/
|
const typeLabels = {
|
sum: '合计',
|
count: '数量',
|
avg: '平均',
|
min: '最小',
|
max: '最大',
|
};
|
|
/**
|
* 固定列样式常量
|
*/
|
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 配置数组
|
* @param {string} props.footerLabel - 第一列显示的标签,默认'合计'
|
*/
|
const StickyTableFooter = ({ footerConfig, footerLabel = '合计' }) => {
|
const data = useDataTableDataContext();
|
const translate = useTranslate();
|
|
// 缓存计算结果,避免不必要的重复计算
|
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 typePrefix = typeLabels[type] || '合计';
|
|
// 支持自定义渲染
|
if (render) {
|
return { label, value: render(value, data), typePrefix };
|
}
|
|
// 获取翻译后的标签
|
const displayLabel = label ? (label.startsWith('table.') || label.startsWith('common.') ? translate(label) : label) : field;
|
return { label: displayLabel, value, typePrefix };
|
}), [footerConfig, data, translate]);
|
|
return (
|
<TableFooter>
|
<TableRow>
|
{results.map((item, index) => (
|
<TableCell key={index} variant="footer" align="left">
|
{item.label} {item.typePrefix}: {item.value}
|
</TableCell>
|
))}
|
<TableCell colSpan={99} />
|
</TableRow>
|
</TableFooter>
|
);
|
};
|
|
/**
|
* StickyDataTable Component
|
*
|
* 封装 react-admin 的 DataTable,实现传入列名即可固定列,支持配置化 footer 汇总。
|
*
|
* @param {Object} props
|
* @param {string[]} props.stickyLeft - 需要固定在左侧的字段 source 列表
|
* @param {string[]} props.stickyRight - 需要固定在右侧的字段 source 列表
|
* @param {Array} props.footerConfig - footer 汇总配置,格式:[{ field: 'anfme', type: 'sum', label: 'table.field.xxx' }]
|
* - field: 要计算的字段名
|
* - type: 计算类型,支持 'sum' | 'count' | 'avg' | 'min' | 'max',默认 'sum'
|
* - label: 显示的标签,支持翻译 key 或直接显示的文本
|
* - render: 可选,自定义渲染函数 (value, data) => ReactNode
|
* @param {string} props.footerLabel - footer 第一列标签,默认'合计'
|
*/
|
export const StickyDataTable = ({
|
stickyLeft = [],
|
stickyRight = [],
|
footerConfig,
|
footerLabel = '合计',
|
children,
|
...props
|
}) => {
|
|
// 使用 Set 优化查找性能 O(n) -> O(1)
|
const stickyLeftSet = useMemo(() => new Set(stickyLeft), [stickyLeft]);
|
const stickyRightSet = useMemo(() => new Set(stickyRight), [stickyRight]);
|
|
// 缓存处理后的 children,避免不必要的重新计算
|
const processedChildren = useMemo(() => {
|
const processChildren = (children) => {
|
return React.Children.map(children, (child) => {
|
if (!React.isValidElement(child)) return child;
|
|
// 如果是 Fragment,递归处理其 children
|
if (child.type === React.Fragment) {
|
return <React.Fragment>{processChildren(child.props.children)}</React.Fragment>;
|
}
|
|
const source = child.props.source;
|
let stickyStyle = null;
|
|
// 左侧固定
|
if (stickyLeftSet.has(source)) {
|
stickyStyle = stickyLeftStyle;
|
}
|
|
// 右侧固定
|
if (stickyRightSet.has(source)) {
|
stickyStyle = stickyRightStyle;
|
}
|
|
if (stickyStyle) {
|
// 合并 sx
|
return React.cloneElement(child, {
|
sx: { ...child.props.sx, ...stickyStyle }
|
});
|
}
|
|
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={tableStyles}>
|
{processedChildren}
|
</DataTable>
|
);
|
};
|
|
export default StickyDataTable;
|