ProTable.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
  2. import { Table, Input, Space, Button } from 'antd';
  3. import { SearchOutlined } from '@ant-design/icons';
  4. import type { TableProps, TablePaginationConfig } from 'antd/es/table'; // eslint-disable-line
  5. import type { SorterResult, FilterValue, ColumnType } from 'antd/es/table/interface';
  6. // 类型定义
  7. export interface ActionType {
  8. reload: (resetPageIndex?: boolean) => void;
  9. reset: () => void;
  10. clearSelected?: () => void;
  11. }
  12. export interface ProColumns<T = any> extends Omit<ColumnType<T>, 'render' | 'filters' | 'onFilter'> {
  13. title?: React.ReactNode;
  14. dataIndex?: string | string[];
  15. key?: string;
  16. width?: number | string;
  17. search?: boolean | SearchConfig;
  18. hideInTable?: boolean;
  19. tooltip?: string;
  20. ellipsis?: boolean;
  21. valueType?: 'text' | 'date' | 'dateTime' | 'option' | 'money' | 'index';
  22. valueEnum?: Record<string, { text: React.ReactNode; status?: string }>;
  23. render?: (
  24. dom: any,
  25. entity: T,
  26. index: number,
  27. action: ActionType,
  28. schema?: ProColumns<T>
  29. ) => React.ReactNode;
  30. filters?: boolean;
  31. onFilter?: boolean | ((value: any, record: T) => boolean);
  32. sorter?: boolean | ((a: T, b: T) => number);
  33. }
  34. interface SearchConfig {
  35. transform?: (value: any) => any;
  36. }
  37. export interface RequestData<T> {
  38. data: T[];
  39. success?: boolean;
  40. total?: number;
  41. }
  42. export interface ProTableProps<T = any> {
  43. columns: ProColumns<T>[];
  44. request?: (
  45. params: Record<string, any>,
  46. sorter: Record<string, any>,
  47. filter: Record<string, any>
  48. ) => Promise<RequestData<T>>;
  49. actionRef?: React.MutableRefObject<ActionType | undefined>;
  50. rowKey?: string | ((record: T) => string);
  51. bordered?: boolean;
  52. pagination?: false | TablePaginationConfig;
  53. search?: false | { labelWidth?: number | 'auto' };
  54. options?: {
  55. search?: boolean;
  56. reload?: boolean;
  57. density?: boolean;
  58. setting?: boolean;
  59. };
  60. toolBarRender?: () => React.ReactNode[];
  61. toolbar?: {
  62. menu?: {
  63. activeKey?: React.Key;
  64. items?: Array<{
  65. key: string;
  66. label: React.ReactNode;
  67. }>;
  68. onChange?: (key: React.Key) => void;
  69. };
  70. };
  71. headerTitle?: React.ReactNode;
  72. params?: Record<string, any>;
  73. }
  74. const ProTable = <T extends Record<string, any>>({
  75. columns,
  76. request,
  77. actionRef,
  78. rowKey = 'id',
  79. bordered = false,
  80. pagination = {},
  81. search = false,
  82. options = {},
  83. toolBarRender,
  84. toolbar,
  85. headerTitle,
  86. params: externalParams,
  87. ...restProps
  88. }: ProTableProps<T>) => {
  89. const [loading, setLoading] = useState(false);
  90. const [dataSource, setDataSource] = useState<T[]>([]);
  91. const [total, setTotal] = useState(0);
  92. const [currentPage, setCurrentPage] = useState(1);
  93. const [pageSize, setPageSize] = useState(
  94. typeof pagination === 'object' ? pagination.defaultPageSize || 20 : 20
  95. );
  96. const [searchKeyword, setSearchKeyword] = useState('');
  97. const [sorter, setSorter] = useState<Record<string, any>>({});
  98. const [filters, setFilters] = useState<Record<string, any>>({});
  99. // 创建内部 ref
  100. const internalActionRef = useRef<ActionType>({
  101. reload: async (resetPageIndex = false) => {
  102. if (resetPageIndex) {
  103. setCurrentPage(1);
  104. }
  105. await fetchData(resetPageIndex ? 1 : currentPage);
  106. },
  107. reset: () => {
  108. setSearchKeyword('');
  109. setCurrentPage(1);
  110. setSorter({});
  111. setFilters({});
  112. },
  113. });
  114. // 暴露 actionRef
  115. useImperativeHandle(actionRef, () => internalActionRef.current);
  116. const fetchData = async (page = currentPage) => {
  117. if (!request) return;
  118. setLoading(true);
  119. try {
  120. const params = {
  121. current: page,
  122. pageSize,
  123. keyword: searchKeyword,
  124. ...externalParams,
  125. };
  126. const result = await request(params, sorter, filters);
  127. setDataSource(result.data || []);
  128. setTotal(result.total || 0);
  129. } catch (error) {
  130. console.error('ProTable fetch error:', error);
  131. } finally {
  132. setLoading(false);
  133. }
  134. };
  135. // 监听参数变化
  136. useEffect(() => {
  137. fetchData(1);
  138. setCurrentPage(1);
  139. }, [searchKeyword, sorter, filters, JSON.stringify(externalParams)]);
  140. // 处理表格变化
  141. const handleTableChange = (
  142. newPagination: TablePaginationConfig,
  143. newFilters: Record<string, FilterValue | null>,
  144. newSorter: SorterResult<T> | SorterResult<T>[]
  145. ) => {
  146. // 处理分页
  147. if (newPagination.current !== currentPage) {
  148. setCurrentPage(newPagination.current || 1);
  149. fetchData(newPagination.current || 1);
  150. }
  151. if (newPagination.pageSize !== pageSize) {
  152. setPageSize(newPagination.pageSize || 20);
  153. setCurrentPage(1);
  154. }
  155. // 处理排序
  156. const sorterResult = Array.isArray(newSorter) ? newSorter[0] : newSorter;
  157. if (sorterResult && sorterResult.field) {
  158. setSorter({
  159. [sorterResult.field as string]: sorterResult.order,
  160. });
  161. } else {
  162. setSorter({});
  163. }
  164. // 处理过滤
  165. const validFilters: Record<string, any> = {};
  166. Object.entries(newFilters).forEach(([key, value]) => {
  167. if (value && value.length > 0) {
  168. validFilters[key] = value;
  169. }
  170. });
  171. setFilters(validFilters);
  172. };
  173. // 转换列配置
  174. const processedColumns = columns
  175. .filter((col) => !col.hideInTable)
  176. .map((col) => {
  177. const processed: any = { ...col };
  178. // 处理 valueEnum 为 filters
  179. if (col.valueEnum && col.filters) {
  180. processed.filters = Object.entries(col.valueEnum).map(([key, value]) => ({
  181. text: value.text,
  182. value: key,
  183. }));
  184. if (col.onFilter) {
  185. processed.onFilter = (value: any, record: T) => {
  186. const dataValue = col.dataIndex
  187. ? record[col.dataIndex as string]
  188. : undefined;
  189. return dataValue === value;
  190. };
  191. }
  192. }
  193. // 处理 valueType
  194. if (col.valueType === 'date' || col.valueType === 'dateTime') {
  195. const originalRender = processed.render;
  196. processed.render = (text: any, record: T, index: number) => {
  197. if (originalRender) {
  198. return originalRender(text, record, index, internalActionRef.current, col);
  199. }
  200. if (!text) return '-';
  201. const date = new Date(text);
  202. if (col.valueType === 'date') {
  203. return date.toLocaleDateString();
  204. }
  205. return date.toLocaleString();
  206. };
  207. }
  208. // 处理自定义 render
  209. if (col.render && processed.render !== col.render) {
  210. const customRender = col.render;
  211. processed.render = (text: any, record: T, index: number) => {
  212. return customRender(text, record, index, internalActionRef.current, col);
  213. };
  214. }
  215. // 处理 ellipsis 和 tooltip
  216. if (col.ellipsis) {
  217. processed.ellipsis = {
  218. showTitle: col.tooltip !== undefined,
  219. };
  220. }
  221. return processed;
  222. });
  223. // 构建工具栏
  224. const renderToolbar = () => {
  225. const menuItems = toolbar?.menu?.items || [];
  226. const activeKey = toolbar?.menu?.activeKey;
  227. const onChange = toolbar?.menu?.onChange;
  228. return (
  229. <div
  230. style={{
  231. display: 'flex',
  232. justifyContent: 'space-between',
  233. alignItems: 'center',
  234. marginBottom: 16,
  235. padding: '16px 0',
  236. }}
  237. >
  238. <div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
  239. {headerTitle}
  240. {menuItems.length > 0 && (
  241. <Space>
  242. {menuItems.map((item) => (
  243. <Button
  244. key={item.key}
  245. type={activeKey === item.key ? 'primary' : 'default'}
  246. onClick={() => onChange?.(item.key)}
  247. >
  248. {item.label}
  249. </Button>
  250. ))}
  251. </Space>
  252. )}
  253. </div>
  254. <Space>{toolBarRender?.()}</Space>
  255. </div>
  256. );
  257. };
  258. // 构建搜索栏
  259. const renderSearch = () => {
  260. if (!options.search) return null;
  261. return (
  262. <div style={{ marginBottom: 16 }}>
  263. <Input.Search
  264. placeholder="请输入关键词搜索"
  265. allowClear
  266. enterButton={<SearchOutlined />}
  267. value={searchKeyword}
  268. onChange={(e) => setSearchKeyword(e.target.value)}
  269. onSearch={(value) => {
  270. setSearchKeyword(value);
  271. setCurrentPage(1);
  272. }}
  273. style={{ maxWidth: 400 }}
  274. />
  275. </div>
  276. );
  277. };
  278. const paginationConfig: TablePaginationConfig | false =
  279. pagination === false
  280. ? false
  281. : {
  282. current: currentPage,
  283. pageSize,
  284. total,
  285. showSizeChanger: true,
  286. showQuickJumper: true,
  287. showTotal: (total) => `共 ${total} 条`,
  288. onChange: (page, newPageSize) => {
  289. setCurrentPage(page);
  290. if (newPageSize !== pageSize) {
  291. setPageSize(newPageSize);
  292. setCurrentPage(1);
  293. }
  294. },
  295. ...pagination,
  296. };
  297. return (
  298. <div className="pro-table">
  299. {renderToolbar()}
  300. {renderSearch()}
  301. <Table<T>
  302. {...restProps}
  303. columns={processedColumns}
  304. dataSource={dataSource}
  305. loading={loading}
  306. rowKey={rowKey}
  307. bordered={bordered}
  308. pagination={paginationConfig}
  309. onChange={handleTableChange}
  310. />
  311. </div>
  312. );
  313. };
  314. export default ProTable;