This commit is contained in:
2025-10-21 03:26:49 +08:00
parent b5430eb8d0
commit df859762ad
19 changed files with 2750 additions and 542 deletions

View File

@@ -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>
);
};