import * as React from 'react'; import { useState, useEffect, useRef, useCallback, useImperativeHandle } from 'react'; import { Loader2, Search, RotateCcw } from 'lucide-react'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from './table'; import { DataTablePagination } from './pagination'; import { Button } from './button'; import { Input } from './input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; import type { Page } from '@/types/base'; import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page'; // 列定义 export interface ColumnDef { key: string; title: React.ReactNode; width?: string; sticky?: boolean; render?: (value: any, record: T, index: number) => React.ReactNode; dataIndex?: string; className?: string; } // 搜索字段定义 export interface SearchFieldDef { key: string; type: 'input' | 'select'; placeholder?: string; options?: { label: string; value: string | number }[]; width?: string; /** 是否支持回车搜索(仅 input 类型) */ enterSearch?: boolean; } // 查询参数基础类型 export interface BaseQuery { pageNum?: number; pageSize?: number; [key: string]: any; } // 组件 Props export interface PaginatedTableProps { fetchFn: (query: Q) => Promise>; columns: ColumnDef[]; /** 搜索字段定义(简化模式) */ searchFields?: SearchFieldDef[]; /** 自定义搜索栏(完全自定义模式) */ searchBar?: React.ReactNode; /** 工具栏(新建按钮等) */ toolbar?: React.ReactNode; /** 初始查询参数 */ initialQuery?: Omit; rowKey?: keyof T | ((record: T) => string | number); minWidth?: string; pageSize?: number; emptyText?: React.ReactNode; loadingText?: React.ReactNode; /** 搜索栏和表格之间的间距 */ searchGap?: string; } // 暴露给外部的方法 export interface PaginatedTableRef { refresh: (showLoading?: boolean) => Promise; reset: () => void; search: () => void; getData: () => Page | null; getQuery: () => Record; updateRecord: (id: string | number, updates: Partial) => void; } function getNestedValue(obj: any, path?: string): any { if (!path) return undefined; return path.split('.').reduce((acc, part) => acc?.[part], obj); } function getRowKey(record: T, rowKey: keyof T | ((record: T) => string | number) | undefined, index: number): string | number { if (!rowKey) return index; if (typeof rowKey === 'function') return rowKey(record); return record[rowKey] as string | number; } function PaginatedTableInner, Q extends BaseQuery = BaseQuery>( props: PaginatedTableProps, ref: React.ForwardedRef> ) { const { fetchFn, columns, searchFields, searchBar, toolbar, initialQuery = {} as Omit, rowKey, minWidth, pageSize: initialPageSize = DEFAULT_PAGE_SIZE, emptyText = '暂无数据', loadingText = '加载中...', searchGap = 'mb-4', } = props; const [data, setData] = useState | null>(null); const [loading, setLoading] = useState(true); // 首次加载显示 loading const [pageNum, setPageNum] = useState(DEFAULT_PAGE_NUM); const [pageSize] = useState(initialPageSize); const [searchValues, setSearchValues] = useState>(initialQuery); const isFirstLoad = useRef(true); // 加载数据(不再控制 loading 状态,只有首次加载显示) const loadData = useCallback(async () => { try { const result = await fetchFn({ ...searchValues, pageNum, pageSize, } as Q); setData(result); } catch (error) { console.error('PaginatedTable 加载数据失败:', error); } finally { if (isFirstLoad.current) { setLoading(false); isFirstLoad.current = false; } } }, [fetchFn, searchValues, pageNum, pageSize]); // 监听分页变化 useEffect(() => { loadData(); }, [pageNum, pageSize]); // 搜索 const handleSearch = useCallback(() => { if (pageNum === DEFAULT_PAGE_NUM) { loadData(); } else { setPageNum(DEFAULT_PAGE_NUM); } }, [pageNum, loadData]); // 重置 const handleReset = useCallback(() => { setSearchValues(initialQuery); setPageNum(DEFAULT_PAGE_NUM); }, [initialQuery]); // 分页切换 const handlePageChange = useCallback((newPageNum: number) => { setPageNum(newPageNum); }, []); // 暴露方法 useImperativeHandle(ref, () => ({ refresh: async () => { await loadData(); }, reset: handleReset, search: handleSearch, getData: () => data, getQuery: () => searchValues, updateRecord: (id, updates) => { setData(prev => { if (!prev) return prev; return { ...prev, content: prev.content.map(item => getRowKey(item, rowKey, -1) === id ? { ...item, ...updates } : item ), }; }); }, }), [data, loadData, handleReset, handleSearch, searchValues, rowKey]); // 渲染搜索字段 const renderSearchField = (field: SearchFieldDef) => { const value = searchValues[field.key]; if (field.type === 'input') { return (
setSearchValues(prev => ({ ...prev, [field.key]: e.target.value }))} onKeyDown={(e) => field.enterSearch !== false && e.key === 'Enter' && handleSearch()} />
); } if (field.type === 'select') { return ( ); } return null; }; // 渲染单元格 const renderCell = (column: ColumnDef, record: T, index: number) => { if (column.render) { const value = column.dataIndex ? getNestedValue(record, column.dataIndex) : undefined; return column.render(value, record, index); } if (column.dataIndex) { return getNestedValue(record, column.dataIndex); } return null; }; const pageCount = data?.totalPages || 0; const hasData = data && data.content && data.content.length > 0; const showSearchBar = searchFields && searchFields.length > 0; return (
{/* 搜索栏 */} {(showSearchBar || searchBar || toolbar) && (
{searchBar || ( <> {searchFields?.map(renderSearchField)} )} {toolbar &&
{toolbar}
}
)} {/* 表格 */}
{columns.map(col => ( {col.title} ))} {loading ? (
{loadingText}
) : hasData ? ( data.content.map((record, index) => ( {columns.map(col => ( {renderCell(col, record, index)} ))} )) ) : ( {emptyText} )}
{/* 分页 */} {pageCount > 0 && ( )}
); } export const PaginatedTable = React.forwardRef(PaginatedTableInner) as < T extends Record, Q extends BaseQuery = BaseQuery >( props: PaginatedTableProps & { ref?: React.ForwardedRef> } ) => React.ReactElement; export default PaginatedTable;