以下是一份 material-ui V5版本的table封装
ts
import React, { forwardRef, useImperativeHandle, useEffect, useState } from 'react';
import {
Table,
TableBody,
TableSortLabel,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
Paper,
Checkbox,
CircularProgress,
Box,
TablePagination,
Grid,
Button
} from '@mui/material';
import { useDispatch } from 'store';
import { useIntl } from 'react-intl';
import { openSnackbar } from 'store/slices/snackbar';
// import { cloneDeep } from 'lodash';
interface IColumn {
slot?: string;
title?: string;
prop: string;
fixed?: string;
minWidth?: number;
sorting?: boolean; // 是否可以排序
titleRender?: (row: any, injectionData?: any) => React.ReactNode;
render?: (row: any, injectionData?: any) => React.ReactNode;
isRowSelectable?: (row: any) => boolean; // 新增属性,用于判断某行是否可以被勾选
align?: 'left' | 'center' | 'right'; // 新增属性,用于指定对齐方式
}
// interface IRequiredParameters {
// [key: string]: any;
// }
interface ItableConfig {
columns: IColumn[];
Api: any; //请求接口方法
fetchConfig?: {};
onSort?: (column: string, order: 'asc' | 'desc') => void;
showSelect?: boolean; // 控制是否显示选择框
initParams?: {};
requestParams?: {};
pagination?: {
fixedColumns?: string[];
sorting?: boolean;
pageSize: number;
pageSizeOptions?: number[];
};
isDisableFirstRequest?: boolean; //是否禁用第一次进入时就请求接口 为true时,需要手动调用request方法
rowKey?: string;
isCacheCheckOptions?: boolean; // 新增属性,控制是否缓存勾选项
dataRootArrKey?: string;
paginationKey?: {
pageSizeKeyName: string;
totalCountKeyName: string;
pageIndexKeyName: string;
};
injectionData?: any; //从外部注入的数据 给内部通讯
dataListKey?: string; // data下面的第二级别list key的名称
maxHeight?: number | string;
onSelectChange?: (selected: any[]) => void; // 新增属性,当选择变化时调用 selectedIds: string[],
showTablePagination?: boolean; //是否展示分页组件
render?: (row: any) => React.ReactNode;
getCurrentStatus?(status: boolean): any; //获取当前是否是loading状态 来同步search的按钮状态
requiredParametersCallBack?: (params: any) => boolean; //搜索必要参数条件
onChangeList?: (data: any[], obj: any) => void; // 获取列表数据回调
onGetListAfter?: (ls?: any[]) => void; // 获取列表成功后调用一下
}
// type Order = 'asc' | 'desc';
export interface TableMethods {
getTableList: (option?: any) => void; // 你希望父组件能调用的方法
resetTableList: (option?: any) => void; // 你希望父组件能调用的方法
clearSelection: () => void; // 新增的方法
getCheckedRows: () => void;
}
const EnhancedTable = forwardRef<TableMethods, ItableConfig>(
(
{
columns,
Api,
showTablePagination = true,
paginationKey = {
pageSizeKeyName: 'pageSize',
totalCountKeyName: 'total',
pageIndexKeyName: 'page'
},
onSort,
initParams = {},
onSelectChange,
showSelect = false,
dataRootArrKey,
rowKey = 'id',
pagination = {
pageSize: 10
},
maxHeight = 610,
requestParams = [],
dataListKey = 'list',
isCacheCheckOptions = false,
isDisableFirstRequest = false,
getCurrentStatus,
requiredParametersCallBack = null,
onChangeList,
onGetListAfter,
injectionData
},
ref
) => {
// 使用 useImperativeHandle 来暴露方法给父组件
useImperativeHandle(ref, () => ({
getTableList(option?: any) {
option ? getList(option) : getList();
},
resetTableList(option?: any) {
// ... 实现你希望父组件能调用的方法
if (page === 0 && rowsPerPage === pagination.pageSize) {
option ? getList(option) : getList();
} else {
setRowsPerPage(pagination.pageSize);
setPage(0);
}
},
getCheckedRows() {
return selected;
},
clearSelection // 暴露新的方法
}));
const [isRestPageIndex, setIsRestPageIndex] = useState(true);
const getStickyStyle = (column: IColumn, index: number): React.CSSProperties => {
if (column.fixed) {
return {
position: 'sticky',
borderLeft: column.fixed === 'right' ? '1px solid rgba(224, 224, 224, 1)' : undefined,
boxShadow: column.fixed === 'right' ? '-2px 0px 3px rgba(0, 0, 0, 0.2)' : undefined,
right: column.fixed === 'right' ? 0 : undefined,
left: column.fixed === 'left' ? 0 : undefined,
backgroundColor: '#fff',
zIndex: 2
};
}
return {};
};
const dispatch = useDispatch();
const intl = useIntl();
const [data, setData] = useState<any[]>([]);
const [rowsPerPage, setRowsPerPage] = useState(pagination.pageSize || 10);
const [loading, setLoading] = useState(false);
const [order, setOrder] = useState<'asc' | 'desc'>('asc');
const [orderBy, setOrderBy] = useState<string | null>(null);
const [totalCount, setTotalCount] = useState(0);
const [selected, setSelected] = useState<any[]>([]);
const [page, setPage] = useState(0);
const defaultErrorMessage = '出了点问题请稍后再试';
// const [lastRequestParams, setLastRequestParams] = useState<any>({});
// 新增的方法来清除所有勾选项
const clearSelection = () => {
setSelected([]);
if (onSelectChange) {
onSelectChange([]);
}
};
// isDisableFirstRequest
const [flagFirst, setFlagFirst] = useState(false);
useEffect(() => {
if (isDisableFirstRequest) {
if (flagFirst) {
getList();
} else {
setFlagFirst(true);
}
} else {
getList();
}
}, [page, rowsPerPage]);
useEffect(() => {
getCurrentStatus && getCurrentStatus(loading);
}, [loading]);
const [isCacheCheckFlag, setIsCacheCheckFlag] = useState(false);
const getList = async (option = {}) => {
if (loading) return;
var ppppageIndex = page + 1;
if (isRestPageIndex) {
setPage(0);
ppppageIndex = 1;
}
try {
var params: any = {
[paginationKey.pageIndexKeyName]: ppppageIndex, //TablePagination ui是从0开始的
[paginationKey.pageSizeKeyName]: rowsPerPage,
...initParams,
...requestParams,
...option
};
if (!!requiredParametersCallBack) {
var fl: boolean = requiredParametersCallBack(params); //返回true才截断
if (fl) {
return false;
}
}
setLoading(true);
// 如果请求参数变化,并且不是因为翻页或修改每页条数(即是一次新的搜索),则清除勾选项
if (showSelect) {
if (isCacheCheckOptions) {
if (
isCacheCheckFlag
// (lastRequestParams[paginationKey.pageIndexKeyName] !== params[paginationKey.pageIndexKeyName] ||
// lastRequestParams[paginationKey.pageSizeKeyName] !== params[paginationKey.pageSizeKeyName])
) {
setIsCacheCheckFlag(false);
}
} else {
clearSelection();
}
}
const res = await Api(params, dispatch, intl);
// // 更新最后一次请求参数
// setLastRequestParams(cloneDeep(params));
setLoading(false);
setIsRestPageIndex(true);
if (res.code === 0) {
const datas: {
[dataListKey: string]: any;
} = res.data;
const ls = datas[dataListKey];
if (ls && Array.isArray(ls)) {
setData(ls);
const ss = paginationKey.totalCountKeyName as string;
var num = datas[ss] as number;
setTotalCount(num); //总页码
onChangeList?.(ls, res);
} else {
setData([]);
setTotalCount(0);
}
onGetListAfter && onGetListAfter(ls);
} else {
setLoading(false);
setData([]);
setTotalCount(0);
onChangeList?.([], res);
dispatch(
openSnackbar({
open: true,
message: res.msg || defaultErrorMessage,
variant: 'alert',
alert: {
color: 'error'
},
close: false,
anchorOrigin: {
vertical: 'top',
horizontal: 'center'
}
})
);
}
} catch (error) {
setLoading(false);
setIsRestPageIndex(true);
console.log('error error error', error);
}
};
const handleSortRequest = (column: string) => {
const isAsc = orderBy === column && order === 'asc';
setOrder(isAsc ? 'desc' : 'asc');
setOrderBy(column);
onSort?.(column, isAsc ? 'desc' : 'asc');
};
const handleClick = (event: React.MouseEvent<unknown>, row: any) => {
const isSelectable = columns.every((column) => (column.isRowSelectable ? column.isRowSelectable(row) : true));
if (!isSelectable) {
// 如果行不可选,直接返回不执行任何操作
return;
}
const selectedIndex = selected.findIndex((r) => r[rowKey] === row[rowKey]);
let newSelected: any[] = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, row);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1));
}
setSelected(newSelected);
onSelectChange?.(newSelected);
};
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
// 仅选择那些满足 isRowSelectable 条件的行
const newSelecteds = data.filter((row) =>
columns.every((column) => (column.isRowSelectable ? column.isRowSelectable(row) : true))
);
setSelected(newSelecteds);
onSelectChange?.(newSelecteds);
} else {
setSelected([]);
onSelectChange?.([]);
}
};
const handleChangePage = (_event: unknown, newPage: number) => {
setIsCacheCheckFlag(true);
setPage(newPage);
setIsRestPageIndex(false);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsCacheCheckFlag(true);
setRowsPerPage(parseInt(event.target.value, 10));
};
return (
<Box>
{showSelect && (
<Box sx={{ minHeight: '38px' }}>
<Grid container>
<Grid item>
<Typography
sx={{
fontSize: '16px',
paddingTop: '5px'
}}
variant="h6"
>
{'已勾选'}:
<Button
sx={{
cursor: 'auto',
padding: '0',
minWidth: '20px'
}}
>
{selected.length}
</Button>
{'项'}
</Typography>
</Grid>
{selected.length > 0 && (
<Grid
item
sx={{
marginLeft: '10px'
}}
>
<Button onClick={clearSelection}> {'取消选择'}</Button>
</Grid>
)}
</Grid>
</Box>
)}
<Paper sx={{ width: '100%', overflow: 'hidden' }}>
<TableContainer component={Paper} sx={{ maxHeight: maxHeight, overflow: 'auto' }}>
<Table
stickyHeader
aria-label="sticky table"
sx={{
minWidth: 750,
tableLayout: 'auto',
'& .MuiTableCell-root': {
borderBottom: '1px solid rgba(224, 224, 224, 1)' // 底部边框
}
}}
>
<TableHead>
<TableRow>
{showSelect && (
<TableCell padding="checkbox">
<Checkbox
indeterminate={selected.length > 0 && selected.length < totalCount}
checked={totalCount > 0 && selected.length === totalCount}
onChange={handleSelectAllClick}
inputProps={{ 'aria-label': 'select all desserts' }}
/>
</TableCell>
)}
{columns.map((column) => (
<TableCell
align={column.align || 'left'} // 使用 align 属性,如果未指定,默认为左对齐
key={column.prop ? column.prop : Math.floor(Math.random() * 10000) + ''}
style={{
minWidth: column?.minWidth,
position: column.fixed ? 'sticky' : undefined,
top: 0, // 确保固定列头在顶部
right: column.fixed === 'right' ? 0 : undefined,
backgroundColor: '#f8fafc', // 确保固定列的背景色不透明
zIndex: column.fixed ? 110 : 1, // 确保固定列在滚动时覆盖其他列,1100 是 MUI 中的 AppBar zIndex
borderLeft: column.fixed === 'right' ? '1px solid rgba(224, 224, 224, 1)' : undefined,
boxShadow: column.fixed === 'right' ? '-2px 0px 3px rgba(0, 0, 0, 0.2)' : undefined
}}
sortDirection={orderBy === column.prop ? order : false}
>
{column.titleRender ? (
column.titleRender(column, injectionData)
) : column.sorting ? (
<TableSortLabel
active={orderBy === column.prop}
direction={orderBy === column.prop ? order : 'asc'}
onClick={() => handleSortRequest(column.prop)}
>
{column.title}
</TableSortLabel>
) : (
column.title
)}
</TableCell>
))}
{columns.map((column) =>
column.slot === 'right' ? (
<TableCell key={column.prop} style={{ minWidth: column.minWidth }}>
{column.title}
</TableCell>
) : null
)}
</TableRow>
</TableHead>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={columns.length + (showSelect ? 1 : 0)} style={{ textAlign: 'center' }}>
<CircularProgress />
</TableCell>
</TableRow>
) : data.length > 0 ? (
data.map((row, index) => {
const isItemSelected = selected.some((r) => r[rowKey] === row[rowKey]);
const labelId = `enhanced-table-checkbox-${index}`;
// 使用 column 中的 isRowSelectable 函数来判断行是否可选,如果没有提供,则默认为可选
const isSelectable = columns.every((column) =>
column.isRowSelectable ? column.isRowSelectable(row) : true
);
return (
<TableRow
hover
onClick={
showSelect
? (event) => {
// 检查该行是否可选
const isSelectable = columns.every((column) =>
column.isRowSelectable ? column.isRowSelectable(row) : true
);
if (isSelectable) {
handleClick(event, row);
}
}
: undefined
}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row[rowKey]}
selected={isItemSelected}
>
{showSelect && (
<TableCell padding="checkbox">
<Checkbox
checked={isItemSelected}
disabled={!isSelectable} // 根据 isSelectable 禁用或启用复选框
inputProps={{ 'aria-labelledby': labelId }}
/>
</TableCell>
)}
{columns.map((column) => (
<TableCell key={`${row[rowKey]}-${column.prop}`} style={getStickyStyle(column, index)}>
{column.render ? column.render(row, injectionData) : row[column?.prop]}
</TableCell>
))}
</TableRow>
);
})
) : (
<TableRow>
<TableCell colSpan={columns.length + (showSelect ? 1 : 0)} align="center">
{'暂无数据'}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{showTablePagination && (
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}>
<TablePagination
rowsPerPageOptions={pagination?.pageSizeOptions || [10, 20, 30, 50, 100]}
component="div"
count={totalCount}
rowsPerPage={rowsPerPage}
page={page}
sx={{
'.MuiTablePagination-toolbar': {
alignItems: 'center', // 确保工具栏中的所有元素都垂直居中
justifyContent: 'flex-end' // 工具栏内的元素靠右对齐
},
'.MuiTablePagination-selectLabel': {
margin: 0 // 移除默认的外边距
},
'.MuiTablePagination-select': {
margin: 0 // 移除默认的外边距
},
'.MuiTablePagination-displayedRows': {
margin: 0 // 移除默认的外边距
},
marginLeft: '-7px !important'
// 你可以根据需要添加更多的样式规则
}}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Box>
)}
</Paper>
</Box>
);
}
);
export default EnhancedTable;