add card
This commit is contained in:
		@@ -1,19 +1,24 @@
 | 
			
		||||
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,
 | 
			
		||||
  pagination,
 | 
			
		||||
  virtualScroll,
 | 
			
		||||
  actions,
 | 
			
		||||
  onSort
 | 
			
		||||
}) => {
 | 
			
		||||
  const rowHeight = virtualScroll?.rowHeight || DEFAULT_ROW_HEIGHT;
 | 
			
		||||
  const [sortState, setSortState] = useState<SortState>({ field: null, order: null });
 | 
			
		||||
  const [currentPage, setCurrentPage] = useState(1);
 | 
			
		||||
 | 
			
		||||
  // 处理排序
 | 
			
		||||
  const handleSort = (field: string) => {
 | 
			
		||||
@@ -46,38 +51,16 @@ export const Table: React.FC<TableProps> = ({
 | 
			
		||||
    });
 | 
			
		||||
  }, [data, sortState]);
 | 
			
		||||
 | 
			
		||||
  // 分页数据
 | 
			
		||||
  const paginatedData = useMemo(() => {
 | 
			
		||||
    if (!pagination) return sortedData;
 | 
			
		||||
    
 | 
			
		||||
    const start = (currentPage - 1) * pagination.pageSize;
 | 
			
		||||
    const end = start + pagination.pageSize;
 | 
			
		||||
    return sortedData.slice(start, end);
 | 
			
		||||
  }, [sortedData, currentPage, pagination]);
 | 
			
		||||
 | 
			
		||||
  // 处理分页变化
 | 
			
		||||
  const handlePageChange = (page: number) => {
 | 
			
		||||
    setCurrentPage(page);
 | 
			
		||||
    if (pagination && typeof pagination === 'object') {
 | 
			
		||||
      pagination.onChange?.(page, pagination.pageSize);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // 处理页大小变化
 | 
			
		||||
  const handlePageSizeChange = (pageSize: number) => {
 | 
			
		||||
    setCurrentPage(1);
 | 
			
		||||
    if (pagination && typeof pagination === 'object') {
 | 
			
		||||
      pagination.onChange?.(1, pageSize);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  // 当前显示的数据(移除分页,直接使用排序后的数据)
 | 
			
		||||
  const displayData = sortedData;
 | 
			
		||||
 | 
			
		||||
  // 全选/取消全选
 | 
			
		||||
  const handleSelectAll = (checked: boolean) => {
 | 
			
		||||
    if (!rowSelection) return;
 | 
			
		||||
    
 | 
			
		||||
    const allKeys = paginatedData.map(item => item.id);
 | 
			
		||||
    const allKeys = displayData.map(item => item.id);
 | 
			
		||||
    const selectedKeys = checked ? allKeys : [];
 | 
			
		||||
    const selectedRows = checked ? paginatedData : [];
 | 
			
		||||
    const selectedRows = checked ? displayData : [];
 | 
			
		||||
    
 | 
			
		||||
    rowSelection.onChange?.(selectedKeys, selectedRows);
 | 
			
		||||
  };
 | 
			
		||||
@@ -100,6 +83,52 @@ export const Table: React.FC<TableProps> = ({
 | 
			
		||||
    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">
 | 
			
		||||
@@ -110,11 +139,11 @@ export const Table: React.FC<TableProps> = ({
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const selectedKeys = rowSelection?.selectedRowKeys || [];
 | 
			
		||||
  const isAllSelected = paginatedData.length > 0 && paginatedData.every(item => selectedKeys.includes(item.id));
 | 
			
		||||
  const isAllSelected = displayData.length > 0 && displayData.every(item => selectedKeys.includes(item.id));
 | 
			
		||||
  const isIndeterminate = selectedKeys.length > 0 && !isAllSelected;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="table-container">
 | 
			
		||||
    <div className="table-container ">
 | 
			
		||||
      {/* 表格工具栏 */}
 | 
			
		||||
      {rowSelection && selectedKeys.length > 0 && (
 | 
			
		||||
        <div className="table-toolbar">
 | 
			
		||||
@@ -134,173 +163,73 @@ export const Table: React.FC<TableProps> = ({
 | 
			
		||||
 | 
			
		||||
      {/* 表格 */}
 | 
			
		||||
      <div className="table-wrapper">
 | 
			
		||||
        <table className="data-table">
 | 
			
		||||
          <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
              {rowSelection && (
 | 
			
		||||
                <th className="selection-column">
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="checkbox"
 | 
			
		||||
                    checked={isAllSelected}
 | 
			
		||||
                    ref={input => {
 | 
			
		||||
                      if (input) input.indeterminate = isIndeterminate;
 | 
			
		||||
                    }}
 | 
			
		||||
                    onChange={(e) => handleSelectAll(e.target.checked)}
 | 
			
		||||
                  />
 | 
			
		||||
                </th>
 | 
			
		||||
              )}
 | 
			
		||||
              {columns.map(column => (
 | 
			
		||||
                <th 
 | 
			
		||||
                  key={column.key}
 | 
			
		||||
                  style={{ width: column.width }}
 | 
			
		||||
                  className={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>
 | 
			
		||||
                </th>
 | 
			
		||||
              ))}
 | 
			
		||||
              {actions && actions.length > 0 && (
 | 
			
		||||
                <th className="actions-column">操作</th>
 | 
			
		||||
              )}
 | 
			
		||||
            </tr>
 | 
			
		||||
          </thead>
 | 
			
		||||
          <tbody>
 | 
			
		||||
            {paginatedData.map((record, index) => (
 | 
			
		||||
              <tr key={record.id} className="table-row">
 | 
			
		||||
                {rowSelection && (
 | 
			
		||||
                  <td className="selection-column">
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="checkbox"
 | 
			
		||||
                      checked={selectedKeys.includes(record.id)}
 | 
			
		||||
                      onChange={(e) => handleRowSelect(record, e.target.checked)}
 | 
			
		||||
                      disabled={rowSelection.getCheckboxProps?.(record)?.disabled}
 | 
			
		||||
                    />
 | 
			
		||||
                  </td>
 | 
			
		||||
                )}
 | 
			
		||||
                {columns.map(column => (
 | 
			
		||||
                  <td key={column.key}>
 | 
			
		||||
                    {column.render 
 | 
			
		||||
                      ? column.render(getNestedValue(record, column.dataIndex), record, index)
 | 
			
		||||
                      : getNestedValue(record, column.dataIndex)
 | 
			
		||||
                    }
 | 
			
		||||
                  </td>
 | 
			
		||||
                ))}
 | 
			
		||||
                {actions && actions.length > 0 && (
 | 
			
		||||
                  <td className="actions-column">
 | 
			
		||||
                    <div className="action-buttons">
 | 
			
		||||
                      {actions.map(action => (
 | 
			
		||||
                        <button
 | 
			
		||||
                          key={action.key}
 | 
			
		||||
                          className={`btn btn-${action.type || 'default'}`}
 | 
			
		||||
                          onClick={() => action.onClick(record)}
 | 
			
		||||
                          disabled={action.disabled?.(record)}
 | 
			
		||||
                          title={action.label}
 | 
			
		||||
                        >
 | 
			
		||||
                          {action.icon && <span className="btn-icon">{action.icon}</span>}
 | 
			
		||||
                          {action.label}
 | 
			
		||||
                        </button>
 | 
			
		||||
                      ))}
 | 
			
		||||
        {/* 固定表头 */}
 | 
			
		||||
        <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>
 | 
			
		||||
                  </td>
 | 
			
		||||
                )}
 | 
			
		||||
              </tr>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            ))}
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
 | 
			
		||||
        {paginatedData.length === 0 && (
 | 
			
		||||
          <div className="empty-state">
 | 
			
		||||
            <div className="empty-icon">📭</div>
 | 
			
		||||
            <p>暂无数据</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* 分页 */}
 | 
			
		||||
      {pagination && (
 | 
			
		||||
        <div className="pagination-wrapper">
 | 
			
		||||
          <div className="pagination-info">
 | 
			
		||||
            {pagination.showTotal && pagination.showTotal(
 | 
			
		||||
              pagination.total,
 | 
			
		||||
              [
 | 
			
		||||
                (currentPage - 1) * pagination.pageSize + 1,
 | 
			
		||||
                Math.min(currentPage * pagination.pageSize, pagination.total)
 | 
			
		||||
              ]
 | 
			
		||||
            {actions && actions.length > 0 && (
 | 
			
		||||
              <div className="actions-column">操作</div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="pagination-controls">
 | 
			
		||||
            <button
 | 
			
		||||
              className="btn btn-default"
 | 
			
		||||
              onClick={() => handlePageChange(currentPage - 1)}
 | 
			
		||||
              disabled={currentPage <= 1}
 | 
			
		||||
            >
 | 
			
		||||
              上一页
 | 
			
		||||
            </button>
 | 
			
		||||
            
 | 
			
		||||
            <div className="page-numbers">
 | 
			
		||||
              {Array.from({ length: Math.ceil(pagination.total / pagination.pageSize) })
 | 
			
		||||
                .map((_, i) => i + 1)
 | 
			
		||||
                .filter(page => {
 | 
			
		||||
                  const distance = Math.abs(page - currentPage);
 | 
			
		||||
                  return distance === 0 || distance <= 2 || page === 1 || page === Math.ceil(pagination.total / pagination.pageSize);
 | 
			
		||||
                })
 | 
			
		||||
                .map((page, index, pages) => {
 | 
			
		||||
                  const prevPage = pages[index - 1];
 | 
			
		||||
                  const showEllipsis = prevPage && page - prevPage > 1;
 | 
			
		||||
                  
 | 
			
		||||
                  return (
 | 
			
		||||
                    <React.Fragment key={page}>
 | 
			
		||||
                      {showEllipsis && <span className="page-ellipsis">...</span>}
 | 
			
		||||
                      <button
 | 
			
		||||
                        className={`btn page-btn ${currentPage === page ? 'active' : ''}`}
 | 
			
		||||
                        onClick={() => handlePageChange(page)}
 | 
			
		||||
                      >
 | 
			
		||||
                        {page}
 | 
			
		||||
                      </button>
 | 
			
		||||
                    </React.Fragment>
 | 
			
		||||
                  );
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <button
 | 
			
		||||
              className="btn btn-default"
 | 
			
		||||
              onClick={() => handlePageChange(currentPage + 1)}
 | 
			
		||||
              disabled={currentPage >= Math.ceil(pagination.total / pagination.pageSize)}
 | 
			
		||||
            >
 | 
			
		||||
              下一页
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          {pagination.showSizeChanger && (
 | 
			
		||||
            <div className="page-size-selector">
 | 
			
		||||
              <select
 | 
			
		||||
                value={pagination.pageSize}
 | 
			
		||||
                onChange={(e) => handlePageSizeChange(Number(e.target.value))}
 | 
			
		||||
              >
 | 
			
		||||
                <option value={10}>10条/页</option>
 | 
			
		||||
                <option value={20}>20条/页</option>
 | 
			
		||||
                <option value={50}>50条/页</option>
 | 
			
		||||
                <option value={100}>100条/页</option>
 | 
			
		||||
              </select>
 | 
			
		||||
        </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>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user