/** * @file Table * @desc 有任意改动请确保你有在 all-function 里给每个功能都过一遍 */ /* eslint-disable @typescript-eslint/naming-convention */ import React, { createElement, PropsWithChildren, ReactElement, useCallback, useEffect, useMemo, useRef } from 'react' import { useTable as useRTable, useSortBy, useFilters, useRowSelect, useRowState, ColumnWithLooseAccessor } from 'react-table' import classnames from 'classnames' import difference from 'lodash/difference' import { usePrefix } from '../ConfigProvider' import Pagination from './Pagination' import { assert, concat } from '../utils' import { createRtColumn } from './column-options' import { Loading } from '../Loading' import { TableInternalColumnOptions, TableColumnOptionsWithoutAccessor, TableColumnOptionsWithAccessor, TableProps, TableSelectionType } from './types' import withCheckboxColumn from './plugins/withSelectionColumn' import { areIdsEquals, children2ColumnOptions, getRowId, getSelectedRowIds } from './utils' import { useUpdate } from '../utils/hooks' import { Nullable } from '../utils/type' import TBody from './TBody' import THead from './THead' import ColumnWidthsProvider from './providers/ColumnWidthsProvider' import ScrollbarWidthProvider from './providers/ScrollbarWidthProvider' import TableProvider, { TableContextValue, useTable } from './providers/TableProvider' import Colgroup from './Colgroup' import HorizontalScrollProvider, { useHorizontalScroll } from './providers/HorizontalScrollProvider' export default function Table(props: TableProps) { const { records, pagination = false, recordIdAccessor, tableHtmlProps: tableHtmlPropsFromProps, selection, loading = false, className, style, border = 'row', zebra = false, size = 'default', empty, fixHead = false, getRowClassName, getCellClassName, onRowClick } = props const fixColumn = useMemo(() => ({ contentWidth: '', ...props.fixColumn }), [props.fixColumn]) assert(function predict() { if (selection != null && recordIdAccessor == null) return false return true }, 'recordIdAccessor is required while using selection.') const tableHtmlProps = useMemo(() => { if (fixColumn.contentWidth) { return { ...tableHtmlPropsFromProps, style: { width: fixColumn.contentWidth, // 如果内容(table)宽度小于容器宽度,拉伸内容以进行平铺展示 minWidth: '100%', ...tableHtmlPropsFromProps?.style } } } return tableHtmlPropsFromProps }, [fixColumn.contentWidth, tableHtmlPropsFromProps]) // columns 引用不能每次 rerender 都发生改变,不然会额外 render 一次直到不变为止 const rtColumns = useMemo>>( () => { const rawColumns: Array> = props.columns ?? children2ColumnOptions(props.children) return rawColumns.map((column, idx) => createRtColumn(column, idx, size)) }, // 内部 rerender 不会导致 children 引用变化 [props.columns, props.children, size] ) const prefix = usePrefix('table') const wrapPrefix = concat(prefix, 'wrap') const fixColumnPrefix = concat(prefix, 'fixed', 'column') const wrapRef = useRef(null) // undefined 表示没有开启行选择 const selectionType: Nullable = selection ? selection.type || 'multiple' : undefined const { getTableProps, getTableBodyProps, headerGroups, rows, columns, prepareRow } = useRTable( { columns: rtColumns, data: records, autoResetPage: false, autoResetSelectedRows: false, defaultCanFilter: true, getRowId: useCallback( (row: R, relativeIndex: number) => getRowId(row, recordIdAccessor) ?? String(relativeIndex), [recordIdAccessor] ), initialState: { selectedRowIds: getSelectedRowIds(selection?.selectedIds ?? []) }, // 处理 react-table 内部状态的地方,如果后面太复杂考虑抽出来 useControlledState: state => { const { selectedRowIds, ...restState } = state useUpdate(() => { // ids 不相同则通知外部 if (!areIdsEquals(selectedRowIds, getSelectedRowIds(selection?.selectedIds ?? []))) { const nextSelectedIds = Object.keys(selectedRowIds).filter(id => selectedRowIds[id]) if (selectionType === 'single') { selection?.onChange?.(difference(nextSelectedIds, selection?.selectedIds)) return } selection?.onChange?.(nextSelectedIds) } }, [selectedRowIds]) return { // 受控 selection 实现方式:被 getToggleRowSelectedProps.onChange 修改为选择状态 // 经过这边处理,拦截一遍,需要把 table 内部 state 还原 selectedRowIds: useMemo( // eslint-disable-next-line react-hooks/exhaustive-deps () => getSelectedRowIds(selection?.selectedIds ?? []), [selection?.selectedIds] ), ...restState } } }, useFilters, useSortBy, useRowSelect, useRowState, selection && withCheckboxColumn(props) || (() => null) ) const tableContextValue: TableContextValue = useMemo(() => ({ size: 'default', prefix, selectionType, fixHead, getContainer: () => wrapRef.current }), [prefix, selectionType, fixHead]) const realBorder: typeof border = zebra ? 'none' : border const tableClassName = classnames( prefix, // 是 fixdHead 则分别传给 thead 和 tbody 的 table,这里不再传 !fixHead && tableHtmlProps?.className, concat(prefix, 'border', realBorder), zebra && concat(prefix, 'zebra'), concat(prefix, 'size', size), fixHead && concat(prefix, 'fixed', 'header'), fixColumn.contentWidth && fixColumnPrefix, fixColumn.contentWidth === 'max-content' && concat(fixColumnPrefix, 'max-content') ) // 显示水平方向边框 const showHorizBorder = realBorder === 'full' return (
{!fixHead && } columns={columns} headerGroups={headerGroups} tableHtmlProps={tableHtmlProps} cellClassName={getCellClassName?.('header')} /> records={records} columns={columns} rows={rows} prepareRow={prepareRow} bodyProps={getTableBodyProps()} empty={empty} tableHtmlProps={tableHtmlProps} getRowClassName={getRowClassName} cellClassName={getCellClassName?.('body')} onRowClick={onRowClick} /> {/* 翻页放到 InnerTable 的容器外,防止和表格一起滚动 */} { pagination && rows.length > 0 && }
) } function ContentWrapper({ children, fixColumnContentWidth }: PropsWithChildren<{ fixColumnContentWidth: string }>) { const { prefix, fixHead } = useTable() const { setPinned, pinnedWidthList } = useHorizontalScroll() const contentRef = useRef(null) const contentPrefix = concat(prefix, 'loading-inner') const contentClassName = classnames(contentPrefix, fixColumnContentWidth && concat(contentPrefix, 'fixed-column')) const onWrapperScroll = useCallback(({ currentTarget }: React.UIEvent) => { if (fixHead) return setPinned(currentTarget, pinnedWidthList) }, [fixHead, pinnedWidthList, setPinned]) useEffect(() => { if (fixHead) return if (contentRef.current) { setPinned(contentRef.current, pinnedWidthList) } }, [fixHead, setPinned, pinnedWidthList]) return (
{children}
) } function InnerTable({ fixHead = false, tableHtmlProps, ...props }: any) { return createElement(fixHead ? 'div' : 'table', { ...fixHead ? {} : tableHtmlProps, ...props }) } // eslint-disable-next-line max-len export function TableColumn(props: TableColumnOptionsWithAccessor): ReactElement | null export function TableColumn(props: TableColumnOptionsWithoutAccessor): ReactElement | null export function TableColumn() { return null } Table.Column = TableColumn