| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- /**
- * 通用历史记录表格组件
- *
- * 可用于任何类型的历史数据展示:
- * - 翻译历史
- * - 对话历史
- * - 任务历史
- * - 操作日志
- * - 订单记录
- * - 等等...
- */
- import React from 'react';
- import { Loader2 } from 'lucide-react';
- import Pagination from './Pagination';
- // ==================== 类型定义 ====================
- /**
- * 历史记录项 - 只要求唯一标识
- */
- export interface HistoryItem {
- id: string | number;
- [key: string]: any;
- }
- /**
- * 列配置
- */
- export interface ColumnConfig<T = HistoryItem> {
- key: string;
- label: string;
- width?: string;
- align?: 'left' | 'center' | 'right';
- render?: (item: T, index: number) => React.ReactNode;
- }
- /**
- * 操作按钮配置
- */
- export interface ActionConfig<T = HistoryItem> {
- icon: React.ComponentType<{ className?: string }>;
- label: string;
- onClick: (item: T, index: number) => void | Promise<void>;
- show?: (item: T, index: number) => boolean;
- disabled?: (item: T, index: number) => boolean;
- loading?: (item: T, index: number) => boolean;
- className?: string;
- variant?: 'default' | 'primary' | 'success' | 'danger' | 'warning';
- }
- /**
- * 组件属性
- */
- export interface HistoryTableProps<T extends HistoryItem = HistoryItem> {
- // 数据
- items: T[];
- total: number;
-
- // 分页
- page: number;
- pageSize: number;
- onPageChange: (page: number) => void;
- pageSizeOptions?: number[];
- onPageSizeChange?: (pageSize: number) => void;
-
- // 列配置
- columns: ColumnConfig<T>[];
-
- // 操作配置
- actions?: ActionConfig<T>[];
- actionsLabel?: string;
- actionsWidth?: string;
-
- // 状态
- isLoading?: boolean;
-
- // 自定义样式
- title?: string;
- emptyText?: string;
- emptyIcon?: React.ReactNode;
- height?: string;
- className?: string;
-
- // 行配置
- rowKey?: keyof T | ((item: T) => string | number);
- onRowClick?: (item: T, index: number) => void;
- rowClassName?: (item: T, index: number) => string;
-
- // 其他
- showPagination?: boolean;
- stickyHeader?: boolean;
- }
- // ==================== 工具函数 ====================
- /**
- * 获取操作按钮的样式类名
- */
- const getActionVariantClass = (variant?: string): string => {
- switch (variant) {
- case 'primary':
- return 'text-blue-600 hover:bg-blue-50';
- case 'success':
- return 'text-green-600 hover:bg-green-50';
- case 'danger':
- return 'text-red-600 hover:bg-red-50';
- case 'warning':
- return 'text-yellow-600 hover:bg-yellow-50';
- default:
- return 'text-gray-400 hover:text-gray-600 hover:bg-gray-50';
- }
- };
- // ==================== 组件 ====================
- /**
- * 通用历史记录表格组件
- */
- function HistoryTable<T extends HistoryItem = HistoryItem>({
- items,
- total,
- page,
- pageSize,
- onPageChange,
- pageSizeOptions = [],
- onPageSizeChange,
- columns,
- actions = [],
- actionsLabel = '操作',
- actionsWidth = '120px',
- isLoading = false,
- title,
- emptyText = '暂无数据',
- emptyIcon,
- height = '500px',
- className = '',
- rowKey = 'id',
- onRowClick,
- rowClassName,
- showPagination = true,
- stickyHeader = true
- }: HistoryTableProps<T>) {
-
- /**
- * 获取行的唯一key
- */
- const getRowKey = (item: T, index: number): string | number => {
- if (typeof rowKey === 'function') {
- return rowKey(item);
- }
- return item[rowKey] as string | number;
- };
-
- /**
- * 获取行的className
- */
- const getRowClassName = (item: T, index: number): string => {
- const baseClass = 'border-b border-gray-100 transition-colors';
- const hoverClass = onRowClick ? 'hover:bg-blue-50 cursor-pointer' : 'hover:bg-gray-50';
- const customClass = rowClassName ? rowClassName(item, index) : '';
- return `${baseClass} ${hoverClass} ${customClass}`.trim();
- };
-
- /**
- * 处理行点击
- */
- const handleRowClick = (item: T, index: number) => {
- if (onRowClick) {
- onRowClick(item, index);
- }
- };
-
- return (
- <div className={`bg-white rounded-2xl border border-gray-100 shadow-sm p-6 ${className}`}>
- {/* 标题 */}
- {title && (
- <div className="flex items-center justify-between mb-4">
- <h3 className="text-lg font-bold text-gray-900">{title}</h3>
- </div>
- )}
-
- {/* 内容区域 */}
- {isLoading ? (
- <div className="text-center py-12">
- <Loader2 className="w-6 h-6 animate-spin text-gray-400 mx-auto mb-2" />
- <p className="text-sm text-gray-400">加载中...</p>
- </div>
- ) : items.length === 0 ? (
- <div className="text-center py-12">
- {emptyIcon && <div className="mb-3">{emptyIcon}</div>}
- <p className="text-sm text-gray-400">{emptyText}</p>
- </div>
- ) : (
- <>
- {/* 表格 */}
- <div className="overflow-x-auto" style={{ height, overflowY: 'auto' }}>
- <table className="w-full text-sm">
- <thead className={stickyHeader ? 'sticky top-0 bg-white z-10' : ''}>
- <tr className="border-b border-gray-200">
- {columns.map((col) => (
- <th
- key={col.key}
- className={`py-3 px-4 font-semibold text-gray-700 ${
- col.align === 'center' ? 'text-center' :
- col.align === 'right' ? 'text-right' :
- 'text-left'
- }`}
- style={{ width: col.width }}
- >
- {col.label}
- </th>
- ))}
- {actions.length > 0 && (
- <th
- className="text-center py-3 px-4 font-semibold text-gray-700"
- style={{ width: actionsWidth }}
- >
- {actionsLabel}
- </th>
- )}
- </tr>
- </thead>
- <tbody>
- {items.map((item, index) => (
- <tr
- key={getRowKey(item, index)}
- className={getRowClassName(item, index)}
- onClick={() => handleRowClick(item, index)}
- >
- {columns.map((col) => (
- <td
- key={col.key}
- className={`py-3 px-4 text-gray-600 ${
- col.align === 'center' ? 'text-center' :
- col.align === 'right' ? 'text-right' :
- 'text-left'
- }`}
- >
- {col.render ? col.render(item, index) : (item as any)[col.key]}
- </td>
- ))}
- {actions.length > 0 && (
- <td className="py-3 px-4" onClick={(e) => e.stopPropagation()}>
- <div className="flex items-center justify-center space-x-2">
- {actions.map((action, actionIndex) => {
- // 检查是否显示
- if (action.show && !action.show(item, index)) {
- return null;
- }
-
- const Icon = action.icon;
- const isDisabled = action.disabled ? action.disabled(item, index) : false;
- const isLoading = action.loading ? action.loading(item, index) : false;
- const variantClass = getActionVariantClass(action.variant);
-
- return (
- <button
- key={actionIndex}
- onClick={() => action.onClick(item, index)}
- disabled={isDisabled || isLoading}
- className={
- action.className ||
- `p-1.5 rounded transition-all disabled:opacity-50 disabled:cursor-not-allowed ${variantClass}`
- }
- title={action.label}
- >
- <Icon className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
- </button>
- );
- })}
- </div>
- </td>
- )}
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- {/* 分页 */}
- {showPagination && (
- <Pagination
- total={total}
- totalPages={Math.ceil(total / pageSize)}
- currentPage={page}
- onPageChange={onPageChange}
- showTotal={true}
- scrollToTop={false}
- pageSize={pageSize}
- pageSizeOptions={pageSizeOptions}
- onPageSizeChange={onPageSizeChange}
- />
- )}
- </>
- )}
- </div>
- );
- }
- export default HistoryTable;
|