import React, { useState, useEffect, useCallback, useRef, useMemo, useImperativeHandle, forwardRef } from 'react'; import { Table as ATable, Pagination, Tooltip } from 'antd'; import { PaginationProps } from 'antd/lib/pagination'; import classnames from 'classnames'; import axios, { CancelTokenSource } from 'axios'; import server from './../../utils/server'; import HeaderCell from './HeaderCell'; import ResizeableTitle from './ResizeableTitle'; import ToolNode from './ToolNode'; import * as utils from './util'; import { TableProps, ColumnProps, TableRefProps } from './interface'; import style from './table.module.scss'; const PAGE_SIZE = 20; const ROW_KEY = 'id'; const MAX_SHOWLINE = 15; const noSortColumnKeys = ['action']; // 不需要sort排序的列 const defScroll = { x: '100%', y: undefined }; const pageSizeOptions = ['20', '50', '100', '200', '500', '800', '1000', '2000']; const showTotal = (total: number) => `${total} total`; const CancelToken = axios.CancelToken; const ColumnTooltip = ({ title }: { title: any }) => ( {title} ); function Table({ url, rowKey, dataSource: propDataSource, query, extra, preserveSelectedRows, toolNode, hanlderResult, hanlderQuery, flat, maxShowLine, onDoubleClick, children, columns: propColumns, ...aTableProps }: TableProps, ref: React.Ref>) { const serverTimeout = useRef(null); const beforeQuery = useRef(); const tableBoxDomRef = useRef(null); const selectedRows = useRef([]); // 记录选中的数据 const originalScrollY = useRef(); // 原始scroll y const currClickRowKey = useRef(''); // 正在单击的row key const rowClickTimeout = useRef(); const prevCancelTokenSource = useRef(null); const [dataSource, setDataSource] = useState([]); const [total, setTotal] = useState(0); const [current, setCurrent] = useState(1); const [pageSize, setPageSize] = useState(PAGE_SIZE); const [loading, setLoading] = useState(false); const [dragWidth, setDragWidth] = useState>({}); const [scroll, setScroll] = useState['scroll']>(defScroll); const [firstInit, setFirstInit] = useState(false); // 标记是否已经首页加载 const [selectedRowKeys, setSelectedRowKeys] = useState([]); // 选中行key const _dataSource = propDataSource || dataSource; const _rowKey = (rowKey || ROW_KEY) as string; useImperativeHandle(ref, () => ({ getDataSource: () => dataSource, getSelectedRows: () => [selectedRows.current, selectedRowKeys], removeSelectedRows: (keys: string[]) => { if (!keys.length) { setSelectedRowKeys([]); selectedRows.current = []; } else { const keysMap = keys.reduce((next, cur) => (next[cur] = true) && next, {}); setSelectedRowKeys(keys => keys.filter(k => !keysMap[k])); selectedRows.current = selectedRows.current.filter(item => !keysMap[item[_rowKey]]); } }, onRefresh: () => { setSelectedRowKeys([]); selectedRows.current = []; if (current !== 1) setCurrent(1); else getListServer(current, pageSize, query); } })); useEffect(() => { if (!firstInit) return; const { offsetHeight, offsetWidth } = tableBoxDomRef.current as HTMLDivElement; // 记录原始高度 if (!originalScrollY.current) { originalScrollY.current = utils.getScrollY(offsetHeight, _dataSource.length) } setScroll({ x: utils.getScrollX(offsetWidth, propColumns), y: originalScrollY.current }) }, [_dataSource, firstInit, toolNode, propColumns]); useEffect(() => { if (toolNode) { setScroll(scroll => ({ x: scroll?.x, y: originalScrollY.current })) } }, [selectedRowKeys, toolNode]); const getListServer = useCallback((page: number, pageSize: number, query?: object) => { if (!url) return setFirstInit(true);; if (serverTimeout.current) { clearTimeout(serverTimeout.current); serverTimeout.current = null; } serverTimeout.current = window.setTimeout(async () => { setLoading(true); let data = { pages: { page, pageSize }, ...query }; if (hanlderQuery) data = hanlderQuery(data); prevCancelTokenSource.current = CancelToken.source(); const result = await server({ url, data, cancelToken: prevCancelTokenSource.current.token }); const handleAfter = hanlderResult ? hanlderResult(result) : result; const isArr = Array.isArray(handleAfter); setDataSource(isArr ? handleAfter : handleAfter.rows); setTotal(isArr ? handleAfter.length : handleAfter.count); setLoading(false); setFirstInit(true); serverTimeout.current = null; }, 200); }, [url, hanlderResult, hanlderQuery]); useEffect(() => { if (beforeQuery.current !== query) { if (!preserveSelectedRows) { setSelectedRowKeys([]); selectedRows.current = []; } prevCancelTokenSource.current?.cancel(); // 取消请求 beforeQuery.current = query; if (current !== 1) return setCurrent(1); } getListServer(current, pageSize, query); }, [url, current, pageSize, query, getListServer, preserveSelectedRows]); const components = useMemo(() => { const curExtra = extra?.some(item => item.headerRender); return { header: { cell: curExtra ? HeaderCell : ResizeableTitle } } }, [extra]); const onResize = useCallback((column: ColumnProps) => { return (_: React.BaseSyntheticEvent, { size }: { size: { width: number, height: number } }) => { if (!column.dataIndex) return; setDragWidth(prev => ({ ...prev, [column.dataIndex as string]: size.width })) } }, []); const render = useCallback((value: any, record: RecordType, index: number, column) => { let handValue = utils.formatValue(value, column.format); const render = column.render || extra?.find((item) => item.dataIndex === column.dataIndex)?.render; if (render) handValue = render(value, record, index); return column.tooltip ? : handValue }, [extra]); const columns = useMemo(() => { return propColumns?.map(item => { const dataIndex = item.dataIndex as string; const sorter = item.sorter === false || noSortColumnKeys.includes(dataIndex) || extra?.some(extraItem => extraItem.dataIndex === dataIndex); return { ...item, sorter: sorter ? false : utils.sortDataSource(item), className: dataIndex, width: dragWidth[dataIndex] || item.width, render: (value: RecordType, record: RecordType, index: number) => render(value, record, index, item), onHeaderCell: (column: ColumnProps) => ({ width: column.width, onResize: onResize(column) } as any) } }) }, [propColumns, dragWidth, onResize, render, extra]); const onSelectAll = useCallback((checked, _, changeRows) => { const keysRecord: string[] = changeRows.map((item: RecordType) => item[_rowKey]); if (checked) { setSelectedRowKeys(keys => keys.concat(keysRecord)); selectedRows.current = selectedRows.current.concat(changeRows); } else { setSelectedRowKeys(keys => keys.filter(item => !keysRecord.includes(item))); selectedRows.current = selectedRows.current.filter(item => !keysRecord.includes(item[_rowKey])); } }, [_rowKey]); const onSelect = useCallback((record, checked) => { if (aTableProps.rowSelectionType === 'radio') { if (checked) { setSelectedRowKeys([record[_rowKey]]); selectedRows.current = [record]; } else { setSelectedRowKeys([]); selectedRows.current = []; } } else { if (checked) { setSelectedRowKeys(keys => keys.concat(record[_rowKey])); selectedRows.current = selectedRows.current.concat(record); } else { setSelectedRowKeys(keys => keys.filter(item => item !== record[_rowKey])); selectedRows.current = selectedRows.current.filter(item => item[_rowKey] !== record[_rowKey]); } } }, [aTableProps.rowSelectionType, _rowKey]); const rowSelection = useMemo(() => { if (!aTableProps.rowSelectionType) return undefined; return { type: aTableProps.rowSelectionType, fixed: true, selectedRowKeys, onSelectAll, onSelect } }, [aTableProps.rowSelectionType, selectedRowKeys, onSelectAll, onSelect]); const currentPageAllSelected = useMemo(() => { return dataSource.every(item => selectedRowKeys.includes(item[_rowKey])) }, [dataSource, selectedRowKeys, _rowKey]); function toolNodeSelectAll(checked: boolean) { onSelectAll(checked, _dataSource, _dataSource) }; function onChangePagination(page: number, _pageSize: number | undefined) { setCurrent(_pageSize !== pageSize ? 1 : page); if (_pageSize) { setPageSize(_pageSize); if (_pageSize < pageSize) setDataSource([]); } if (aTableProps.pagination) { (aTableProps.pagination as PaginationProps).onChange?.(page, _pageSize); } }; const _scroll = useMemo(() => { if (aTableProps.scroll?.y) { return { ...scroll, ...aTableProps.scroll } } if (flat && maxShowLine) { return { x: scroll?.x, y: (typeof maxShowLine === 'number' ? maxShowLine : MAX_SHOWLINE) * utils.ROW_HEIGHT } } return scroll }, [scroll, flat, maxShowLine, aTableProps.scroll]); function _onRow(record: RecordType) { return { onClick(e: React.BaseSyntheticEvent) { if (!aTableProps.rowSelectionType && !onDoubleClick) return; if ( e.target.classList.contains('ant-table-cell') || e.target.classList.contains('ant-tooltip-open') || e.target.dataset.click ) { const clear = () => { currClickRowKey.current = ''; clearTimeout(rowClickTimeout.current); rowClickTimeout.current = undefined; } const onClick = () => { if (aTableProps.rowSelectionType === 'radio') { return setSelectedRowKeys(prev => { selectedRows.current = prev.length ? [] : [record]; return prev.length ? [] : [record[_rowKey]] }) } setSelectedRowKeys(prev => { if (prev.some(item => item === record[_rowKey])) { selectedRows.current = selectedRows.current.filter(item => item[_rowKey] !== record[_rowKey]); return prev.filter(item => item !== record[_rowKey]) } selectedRows.current = selectedRows.current.concat(record); return prev.concat(record[_rowKey]) }) } if (!onDoubleClick) return onClick(); if (currClickRowKey.current === record[_rowKey]) { // 双击 onDoubleClick?.(record); clear(); } else { currClickRowKey.current = record[_rowKey]; rowClickTimeout.current = window.setTimeout(() => { onClick(); clear(); }, 300); } } } } }; return (
{aTableProps.pagination !== false && (
)} {toolNode && selectedRowKeys.length ? ( <>
) : null}
) } const ForwardTable = forwardRef(Table) as ( props: React.PropsWithChildren> & { ref?: React.Ref> }, ) => React.ReactElement; export default ForwardTable;