235 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			235 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import React, { useState, useMemo } from 'react';
 | 
						|
import { AutoSizer, List } from 'react-virtualized';
 | 
						|
import { Mark } from '../mock/collection';
 | 
						|
import { TableProps, SortState } from './types';
 | 
						|
import './table.css';
 | 
						|
 | 
						|
// 虚拟滚动常量
 | 
						|
const DEFAULT_ROW_HEIGHT = 48; // 每行高度
 | 
						|
const HEADER_HEIGHT = 48; // 表头高度
 | 
						|
 | 
						|
export const Table: React.FC<TableProps> = ({
 | 
						|
  data,
 | 
						|
  columns,
 | 
						|
  loading = false,
 | 
						|
  rowSelection,
 | 
						|
  virtualScroll,
 | 
						|
  actions,
 | 
						|
  onSort
 | 
						|
}) => {
 | 
						|
  const rowHeight = virtualScroll?.rowHeight || DEFAULT_ROW_HEIGHT;
 | 
						|
  const [sortState, setSortState] = useState<SortState>({ field: null, order: null });
 | 
						|
 | 
						|
  // 处理排序
 | 
						|
  const handleSort = (field: string) => {
 | 
						|
    let newOrder: 'asc' | 'desc' | null = 'asc';
 | 
						|
    
 | 
						|
    if (sortState.field === field) {
 | 
						|
      if (sortState.order === 'asc') {
 | 
						|
        newOrder = 'desc';
 | 
						|
      } else if (sortState.order === 'desc') {
 | 
						|
        newOrder = null;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    
 | 
						|
    const newSortState = { field: newOrder ? field : null, order: newOrder };
 | 
						|
    setSortState(newSortState);
 | 
						|
    onSort?.(newSortState.field!, newSortState.order!);
 | 
						|
  };
 | 
						|
 | 
						|
  // 排序后的数据
 | 
						|
  const sortedData = useMemo(() => {
 | 
						|
    if (!sortState.field || !sortState.order) return data;
 | 
						|
    
 | 
						|
    return [...data].sort((a, b) => {
 | 
						|
      const aVal = getNestedValue(a, sortState.field!);
 | 
						|
      const bVal = getNestedValue(b, sortState.field!);
 | 
						|
      
 | 
						|
      if (aVal < bVal) return sortState.order === 'asc' ? -1 : 1;
 | 
						|
      if (aVal > bVal) return sortState.order === 'asc' ? 1 : -1;
 | 
						|
      return 0;
 | 
						|
    });
 | 
						|
  }, [data, sortState]);
 | 
						|
 | 
						|
  // 当前显示的数据(移除分页,直接使用排序后的数据)
 | 
						|
  const displayData = sortedData;
 | 
						|
 | 
						|
  // 全选/取消全选
 | 
						|
  const handleSelectAll = (checked: boolean) => {
 | 
						|
    if (!rowSelection) return;
 | 
						|
    
 | 
						|
    const allKeys = displayData.map(item => item.id);
 | 
						|
    const selectedKeys = checked ? allKeys : [];
 | 
						|
    const selectedRows = checked ? displayData : [];
 | 
						|
    
 | 
						|
    rowSelection.onChange?.(selectedKeys, selectedRows);
 | 
						|
  };
 | 
						|
 | 
						|
  // 单行选择
 | 
						|
  const handleRowSelect = (record: Mark, checked: boolean) => {
 | 
						|
    if (!rowSelection) return;
 | 
						|
    
 | 
						|
    const currentKeys = rowSelection.selectedRowKeys || [];
 | 
						|
    const newKeys = checked 
 | 
						|
      ? [...currentKeys, record.id]
 | 
						|
      : currentKeys.filter(key => key !== record.id);
 | 
						|
    
 | 
						|
    const selectedRows = data.filter(item => newKeys.includes(item.id));
 | 
						|
    rowSelection.onChange?.(newKeys, selectedRows);
 | 
						|
  };
 | 
						|
 | 
						|
  // 获取嵌套值
 | 
						|
  const getNestedValue = (obj: any, path: string) => {
 | 
						|
    return path.split('.').reduce((o, p) => o?.[p], obj);
 | 
						|
  };
 | 
						|
 | 
						|
  // 渲染虚拟滚动行
 | 
						|
  const rowRenderer = ({ index, key, style }: any) => {
 | 
						|
    const record = displayData[index];
 | 
						|
    
 | 
						|
    return (
 | 
						|
      <div key={key} style={style} className="table-row virtual-row">
 | 
						|
        {rowSelection && (
 | 
						|
          <div className="selection-column">
 | 
						|
            <input
 | 
						|
              type="checkbox"
 | 
						|
              checked={selectedKeys.includes(record.id)}
 | 
						|
              onChange={(e) => handleRowSelect(record, e.target.checked)}
 | 
						|
              disabled={rowSelection.getCheckboxProps?.(record)?.disabled}
 | 
						|
            />
 | 
						|
          </div>
 | 
						|
        )}
 | 
						|
        {columns.map(column => (
 | 
						|
          <div key={column.key} className="table-cell" style={{ width: column.width }}>
 | 
						|
            {column.render 
 | 
						|
              ? column.render(getNestedValue(record, column.dataIndex), record, index)
 | 
						|
              : getNestedValue(record, column.dataIndex)
 | 
						|
            }
 | 
						|
          </div>
 | 
						|
        ))}
 | 
						|
        {actions && actions.length > 0 && (
 | 
						|
          <div className="actions-column">
 | 
						|
            <div className="action-buttons">
 | 
						|
              {actions.map(action => (
 | 
						|
                <button
 | 
						|
                  key={action.key}
 | 
						|
                  className={action.className}
 | 
						|
                  onClick={() => action.onClick(record)}
 | 
						|
                  disabled={action.disabled?.(record)}
 | 
						|
                  aria-label={action.tooltip || action.label}
 | 
						|
                >
 | 
						|
                  {action.icon && <span className="btn-icon">{action.icon}</span>}
 | 
						|
                  <span className="tooltip">{action.tooltip || action.label}</span>
 | 
						|
                </button>
 | 
						|
              ))}
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
        )}
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  };
 | 
						|
 | 
						|
  if (loading) {
 | 
						|
    return (
 | 
						|
      <div className="table-loading">
 | 
						|
        <div className="loading-spinner"></div>
 | 
						|
        <span>加载中...</span>
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  const selectedKeys = rowSelection?.selectedRowKeys || [];
 | 
						|
  const isAllSelected = displayData.length > 0 && displayData.every(item => selectedKeys.includes(item.id));
 | 
						|
  const isIndeterminate = selectedKeys.length > 0 && !isAllSelected;
 | 
						|
 | 
						|
  return (
 | 
						|
    <div className="table-container ">
 | 
						|
      {/* 表格工具栏 */}
 | 
						|
      {rowSelection && selectedKeys.length > 0 && (
 | 
						|
        <div className="table-toolbar">
 | 
						|
          <span className="selected-info">
 | 
						|
            已选择 {selectedKeys.length} 项
 | 
						|
          </span>
 | 
						|
          <div className="bulk-actions">
 | 
						|
            <button className="btn btn-danger" onClick={() => {
 | 
						|
              // 批量删除逻辑
 | 
						|
              console.log('批量删除:', selectedKeys);
 | 
						|
            }}>
 | 
						|
              批量删除
 | 
						|
            </button>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
      )}
 | 
						|
 | 
						|
      {/* 表格 */}
 | 
						|
      <div className="table-wrapper">
 | 
						|
        {/* 固定表头 */}
 | 
						|
        <div className="table-header-wrapper" style={{ height: HEADER_HEIGHT }}>
 | 
						|
          <div className="table-header-row">
 | 
						|
            {rowSelection && (
 | 
						|
              <div className="selection-column">
 | 
						|
                <input
 | 
						|
                  type="checkbox"
 | 
						|
                  checked={isAllSelected}
 | 
						|
                  ref={input => {
 | 
						|
                    if (input) input.indeterminate = isIndeterminate;
 | 
						|
                  }}
 | 
						|
                  onChange={(e) => handleSelectAll(e.target.checked)}
 | 
						|
                />
 | 
						|
              </div>
 | 
						|
            )}
 | 
						|
            {columns.map(column => (
 | 
						|
              <div 
 | 
						|
                key={column.key}
 | 
						|
                style={{ width: column.width }}
 | 
						|
                className={`table-header-cell ${column.sortable ? 'sortable' : ''}`}
 | 
						|
              >
 | 
						|
                <div className="table-header">
 | 
						|
                  <span>{column.title}</span>
 | 
						|
                  {column.sortable && (
 | 
						|
                    <div 
 | 
						|
                      className="sort-indicators"
 | 
						|
                      onClick={() => handleSort(column.dataIndex)}
 | 
						|
                    >
 | 
						|
                      <span className={`sort-arrow sort-up ${
 | 
						|
                        sortState.field === column.dataIndex && sortState.order === 'asc' ? 'active' : ''
 | 
						|
                      }`}>▲</span>
 | 
						|
                      <span className={`sort-arrow sort-down ${
 | 
						|
                        sortState.field === column.dataIndex && sortState.order === 'desc' ? 'active' : ''
 | 
						|
                      }`}>▼</span>
 | 
						|
                    </div>
 | 
						|
                  )}
 | 
						|
                </div>
 | 
						|
              </div>
 | 
						|
            ))}
 | 
						|
            {actions && actions.length > 0 && (
 | 
						|
              <div className="actions-column">操作</div>
 | 
						|
            )}
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        {/* 虚拟滚动内容区域 */}
 | 
						|
        <div className="table-body-wrapper">
 | 
						|
          {displayData.length > 0 ? (
 | 
						|
            <AutoSizer>
 | 
						|
              {({ height, width }) => (
 | 
						|
                <List
 | 
						|
                  height={height}
 | 
						|
                  width={width}
 | 
						|
                  rowCount={displayData.length}
 | 
						|
                  rowHeight={rowHeight}
 | 
						|
                  rowRenderer={rowRenderer}
 | 
						|
                />
 | 
						|
              )}
 | 
						|
            </AutoSizer>
 | 
						|
          ) : (
 | 
						|
            <div className="empty-state">
 | 
						|
              <div className="empty-icon">📭</div>
 | 
						|
              <p>暂无数据</p>
 | 
						|
            </div>
 | 
						|
          )}
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
}; |