HistoryTable.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /**
  2. * 通用历史记录表格组件
  3. *
  4. * 可用于任何类型的历史数据展示:
  5. * - 翻译历史
  6. * - 对话历史
  7. * - 任务历史
  8. * - 操作日志
  9. * - 订单记录
  10. * - 等等...
  11. */
  12. import React from 'react';
  13. import { Loader2 } from 'lucide-react';
  14. import Pagination from './Pagination';
  15. // ==================== 类型定义 ====================
  16. /**
  17. * 历史记录项 - 只要求唯一标识
  18. */
  19. export interface HistoryItem {
  20. id: string | number;
  21. [key: string]: any;
  22. }
  23. /**
  24. * 列配置
  25. */
  26. export interface ColumnConfig<T = HistoryItem> {
  27. key: string;
  28. label: string;
  29. width?: string;
  30. align?: 'left' | 'center' | 'right';
  31. render?: (item: T, index: number) => React.ReactNode;
  32. }
  33. /**
  34. * 操作按钮配置
  35. */
  36. export interface ActionConfig<T = HistoryItem> {
  37. icon: React.ComponentType<{ className?: string }>;
  38. label: string;
  39. onClick: (item: T, index: number) => void | Promise<void>;
  40. show?: (item: T, index: number) => boolean;
  41. disabled?: (item: T, index: number) => boolean;
  42. loading?: (item: T, index: number) => boolean;
  43. className?: string;
  44. variant?: 'default' | 'primary' | 'success' | 'danger' | 'warning';
  45. }
  46. /**
  47. * 组件属性
  48. */
  49. export interface HistoryTableProps<T extends HistoryItem = HistoryItem> {
  50. // 数据
  51. items: T[];
  52. total: number;
  53. // 分页
  54. page: number;
  55. pageSize: number;
  56. onPageChange: (page: number) => void;
  57. pageSizeOptions?: number[];
  58. onPageSizeChange?: (pageSize: number) => void;
  59. // 列配置
  60. columns: ColumnConfig<T>[];
  61. // 操作配置
  62. actions?: ActionConfig<T>[];
  63. actionsLabel?: string;
  64. actionsWidth?: string;
  65. // 状态
  66. isLoading?: boolean;
  67. // 自定义样式
  68. title?: string;
  69. emptyText?: string;
  70. emptyIcon?: React.ReactNode;
  71. height?: string;
  72. className?: string;
  73. // 行配置
  74. rowKey?: keyof T | ((item: T) => string | number);
  75. onRowClick?: (item: T, index: number) => void;
  76. rowClassName?: (item: T, index: number) => string;
  77. // 其他
  78. showPagination?: boolean;
  79. stickyHeader?: boolean;
  80. }
  81. // ==================== 工具函数 ====================
  82. /**
  83. * 获取操作按钮的样式类名
  84. */
  85. const getActionVariantClass = (variant?: string): string => {
  86. switch (variant) {
  87. case 'primary':
  88. return 'text-blue-600 hover:bg-blue-50';
  89. case 'success':
  90. return 'text-green-600 hover:bg-green-50';
  91. case 'danger':
  92. return 'text-red-600 hover:bg-red-50';
  93. case 'warning':
  94. return 'text-yellow-600 hover:bg-yellow-50';
  95. default:
  96. return 'text-gray-400 hover:text-gray-600 hover:bg-gray-50';
  97. }
  98. };
  99. // ==================== 组件 ====================
  100. /**
  101. * 通用历史记录表格组件
  102. */
  103. function HistoryTable<T extends HistoryItem = HistoryItem>({
  104. items,
  105. total,
  106. page,
  107. pageSize,
  108. onPageChange,
  109. pageSizeOptions = [],
  110. onPageSizeChange,
  111. columns,
  112. actions = [],
  113. actionsLabel = '操作',
  114. actionsWidth = '120px',
  115. isLoading = false,
  116. title,
  117. emptyText = '暂无数据',
  118. emptyIcon,
  119. height = '500px',
  120. className = '',
  121. rowKey = 'id',
  122. onRowClick,
  123. rowClassName,
  124. showPagination = true,
  125. stickyHeader = true
  126. }: HistoryTableProps<T>) {
  127. /**
  128. * 获取行的唯一key
  129. */
  130. const getRowKey = (item: T, index: number): string | number => {
  131. if (typeof rowKey === 'function') {
  132. return rowKey(item);
  133. }
  134. return item[rowKey] as string | number;
  135. };
  136. /**
  137. * 获取行的className
  138. */
  139. const getRowClassName = (item: T, index: number): string => {
  140. const baseClass = 'border-b border-gray-100 transition-colors';
  141. const hoverClass = onRowClick ? 'hover:bg-blue-50 cursor-pointer' : 'hover:bg-gray-50';
  142. const customClass = rowClassName ? rowClassName(item, index) : '';
  143. return `${baseClass} ${hoverClass} ${customClass}`.trim();
  144. };
  145. /**
  146. * 处理行点击
  147. */
  148. const handleRowClick = (item: T, index: number) => {
  149. if (onRowClick) {
  150. onRowClick(item, index);
  151. }
  152. };
  153. return (
  154. <div className={`bg-white rounded-2xl border border-gray-100 shadow-sm p-6 ${className}`}>
  155. {/* 标题 */}
  156. {title && (
  157. <div className="flex items-center justify-between mb-4">
  158. <h3 className="text-lg font-bold text-gray-900">{title}</h3>
  159. </div>
  160. )}
  161. {/* 内容区域 */}
  162. {isLoading ? (
  163. <div className="text-center py-12">
  164. <Loader2 className="w-6 h-6 animate-spin text-gray-400 mx-auto mb-2" />
  165. <p className="text-sm text-gray-400">加载中...</p>
  166. </div>
  167. ) : items.length === 0 ? (
  168. <div className="text-center py-12">
  169. {emptyIcon && <div className="mb-3">{emptyIcon}</div>}
  170. <p className="text-sm text-gray-400">{emptyText}</p>
  171. </div>
  172. ) : (
  173. <>
  174. {/* 表格 */}
  175. <div className="overflow-x-auto" style={{ height, overflowY: 'auto' }}>
  176. <table className="w-full text-sm">
  177. <thead className={stickyHeader ? 'sticky top-0 bg-white z-10' : ''}>
  178. <tr className="border-b border-gray-200">
  179. {columns.map((col) => (
  180. <th
  181. key={col.key}
  182. className={`py-3 px-4 font-semibold text-gray-700 ${
  183. col.align === 'center' ? 'text-center' :
  184. col.align === 'right' ? 'text-right' :
  185. 'text-left'
  186. }`}
  187. style={{ width: col.width }}
  188. >
  189. {col.label}
  190. </th>
  191. ))}
  192. {actions.length > 0 && (
  193. <th
  194. className="text-center py-3 px-4 font-semibold text-gray-700"
  195. style={{ width: actionsWidth }}
  196. >
  197. {actionsLabel}
  198. </th>
  199. )}
  200. </tr>
  201. </thead>
  202. <tbody>
  203. {items.map((item, index) => (
  204. <tr
  205. key={getRowKey(item, index)}
  206. className={getRowClassName(item, index)}
  207. onClick={() => handleRowClick(item, index)}
  208. >
  209. {columns.map((col) => (
  210. <td
  211. key={col.key}
  212. className={`py-3 px-4 text-gray-600 ${
  213. col.align === 'center' ? 'text-center' :
  214. col.align === 'right' ? 'text-right' :
  215. 'text-left'
  216. }`}
  217. >
  218. {col.render ? col.render(item, index) : (item as any)[col.key]}
  219. </td>
  220. ))}
  221. {actions.length > 0 && (
  222. <td className="py-3 px-4" onClick={(e) => e.stopPropagation()}>
  223. <div className="flex items-center justify-center space-x-2">
  224. {actions.map((action, actionIndex) => {
  225. // 检查是否显示
  226. if (action.show && !action.show(item, index)) {
  227. return null;
  228. }
  229. const Icon = action.icon;
  230. const isDisabled = action.disabled ? action.disabled(item, index) : false;
  231. const isLoading = action.loading ? action.loading(item, index) : false;
  232. const variantClass = getActionVariantClass(action.variant);
  233. return (
  234. <button
  235. key={actionIndex}
  236. onClick={() => action.onClick(item, index)}
  237. disabled={isDisabled || isLoading}
  238. className={
  239. action.className ||
  240. `p-1.5 rounded transition-all disabled:opacity-50 disabled:cursor-not-allowed ${variantClass}`
  241. }
  242. title={action.label}
  243. >
  244. <Icon className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
  245. </button>
  246. );
  247. })}
  248. </div>
  249. </td>
  250. )}
  251. </tr>
  252. ))}
  253. </tbody>
  254. </table>
  255. </div>
  256. {/* 分页 */}
  257. {showPagination && (
  258. <Pagination
  259. total={total}
  260. totalPages={Math.ceil(total / pageSize)}
  261. currentPage={page}
  262. onPageChange={onPageChange}
  263. showTotal={true}
  264. scrollToTop={false}
  265. pageSize={pageSize}
  266. pageSizeOptions={pageSizeOptions}
  267. onPageSizeChange={onPageSizeChange}
  268. />
  269. )}
  270. </>
  271. )}
  272. </div>
  273. );
  274. }
  275. export default HistoryTable;