react + ts + material-ui V5版本的table封装

以下是一份 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;
相关推荐
栈老师不回家40 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
帅比九日3 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js