
@[toc]
一、组件设计分析
1.1 功能需求分解
- 核心功能 :
- 数据展示(基础表格)
- 分页控制
- 列排序(升序/降序)
- 条件筛选
- 扩展功能 :
- 自定义列渲染
- 响应式布局
- 多选操作
- 加载状态
1.2 组件接口设计
typescript
interface TableProps {
columns: ColumnDef[]; // 列配置
data: any[]; // 数据源
pagination?: PaginationConfig; // 分页配置
sortable?: boolean; // 是否可排序
filterable?: boolean; // 是否可筛选
loading?: boolean; // 加载状态
rowKey?: string; // 行唯一标识
onPageChange?: (page: number, size: number) => void;
onSort?: (field: string, order: 'asc' | 'desc') => void;
onFilter?: (filters: Record<string, any>) => void;
}
interface ColumnDef {
field: string; // 字段名
header: string | ReactNode; // 表头
width?: number | string; // 列宽
sortable?: boolean; // 是否可排序
filterType?: 'text' | 'select' | 'date'; // 筛选类型
filterOptions?: { value: any; label: string }[]; // 筛选选项
render?: (value: any, record: any) => ReactNode; // 自定义渲染
}
二、基础表格实现(React示例)
2.1 组件骨架
jsx
import React, { useState } from 'react';
import PropTypes from 'prop-types';
const Table = ({
columns,
data,
pagination = { current: 1, pageSize: 10 },
sortable = false,
filterable = false,
loading = false,
rowKey = 'id',
onPageChange,
onSort,
onFilter
}) => {
const [sortField, setSortField] = useState('');
const [sortOrder, setSortOrder] = useState(null);
const [filters, setFilters] = useState({});
// 处理排序
const handleSort = (field) => {
let order = 'asc';
if (sortField === field && sortOrder === 'asc') {
order = 'desc';
} else if (sortField === field && sortOrder === 'desc') {
order = null;
field = '';
}
setSortField(field);
setSortOrder(order);
onSort?.(field, order);
};
// 处理筛选
const handleFilter = (field, value) => {
const newFilters = { ...filters, [field]: value };
setFilters(newFilters);
onFilter?.(newFilters);
};
// 渲染表头
const renderHeader = () => (
<thead>
<tr>
{columns.map((column) => (
<th
key={column.field}
style={{ width: column.width }}
onClick={() => column.sortable && handleSort(column.field)}
>
<div className="header-content">
{column.header}
{sortable && column.sortable && (
<span className="sort-icon">
{sortField === column.field && sortOrder === 'asc' && '↑'}
{sortField === column.field && sortOrder === 'desc' && '↓'}
</span>
)}
</div>
{filterable && column.filterType && renderFilter(column)}
</th>
))}
</tr>
</thead>
);
// 渲染筛选器
const renderFilter = (column) => (
<div className="filter-control">
{column.filterType === 'text' && (
<input
type="text"
onChange={(e) => handleFilter(column.field, e.target.value)}
placeholder={`筛选 ${column.header}`}
/>
)}
{column.filterType === 'select' && (
<select onChange={(e) => handleFilter(column.field, e.target.value)}>
<option value="">全部</option>
{column.filterOptions?.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
)}
</div>
);
// 渲染分页器
const renderPagination = () => (
<div className="pagination">
<button
disabled={pagination.current === 1}
onClick={() => onPageChange?.(pagination.current - 1, pagination.pageSize)}
>
上一页
</button>
<span>
第 {pagination.current} 页 / 共 {Math.ceil(data.length / pagination.pageSize)} 页
</span>
<button
disabled={pagination.current * pagination.pageSize >= data.length}
onClick={() => onPageChange?.(pagination.current + 1, pagination.pageSize)}
>
下一页
</button>
<select
value={pagination.pageSize}
onChange={(e) => onPageChange?.(1, Number(e.target.value))}
>
{[10, 20, 50, 100].map((size) => (
<option key={size} value={size}>
每页 {size} 条
</option>
))}
</select>
</div>
);
return (
<div className="table-container">
<div className="table-wrapper">
<table>
{renderHeader()}
<tbody>
{loading ? (
<tr>
<td colSpan={columns.length} className="loading">
加载中...
</td>
</tr>
) : data.length === 0 ? (
<tr>
<td colSpan={columns.length} className="empty">
暂无数据
</td>
</tr>
) : (
data.map((record) => (
<tr key={record[rowKey]}>
{columns.map((column) => (
<td key={`${record[rowKey]}-${column.field}`}>
{column.render
? column.render(record[column.field], record)
: record[column.field]}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
{pagination && renderPagination()}
</div>
);
};
Table.propTypes = {
columns: PropTypes.arrayOf(
PropTypes.shape({
field: PropTypes.string.isRequired,
header: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
sortable: PropTypes.bool,
filterType: PropTypes.oneOf(['text', 'select', 'date']),
filterOptions: PropTypes.array,
render: PropTypes.func
})
).isRequired,
data: PropTypes.array.isRequired,
pagination: PropTypes.shape({
current: PropTypes.number,
pageSize: PropTypes.number
}),
sortable: PropTypes.bool,
filterable: PropTypes.bool,
loading: PropTypes.bool,
rowKey: PropTypes.string,
onPageChange: PropTypes.func,
onSort: PropTypes.func,
onFilter: PropTypes.func
};
export default Table;
三、关键功能实现细节
3.1 分页控制逻辑
javascript
// 计算当前页数据(客户端分页)
const getPagedData = () => {
if (!pagination) return data;
const { current, pageSize } = pagination;
const start = (current - 1) * pageSize;
const end = start + pageSize;
return data.slice(start, end);
};
// 分页器实现
const Pagination = ({ current, total, pageSize, onChange }) => {
const totalPages = Math.ceil(total / pageSize);
return (
<div className="pagination">
<button
disabled={current === 1}
onClick={() => onChange(1, pageSize)}
>
<<
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (current <= 3) {
pageNum = i + 1;
} else if (current >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = current - 2 + i;
}
return (
<button
key={pageNum}
className={current === pageNum ? 'active' : ''}
onClick={() => onChange(pageNum, pageSize)}
>
{pageNum}
</button>
);
})}
<button
disabled={current === totalPages}
onClick={() => onChange(totalPages, pageSize)}
>
>>
</button>
<span>共 {total} 条</span>
<select
value={pageSize}
onChange={(e) => onChange(1, Number(e.target.value))}
>
{[10, 20, 50, 100].map(size => (
<option key={size} value={size}>{size} 条/页</option>
))}
</select>
</div>
);
};
3.2 排序功能实现
javascript
// 排序逻辑(客户端排序)
const getSortedData = () => {
if (!sortField || !sortOrder) return data;
return [...data].sort((a, b) => {
const valA = a[sortField];
const valB = b[sortField];
if (typeof valA === 'string' && typeof valB === 'string') {
return sortOrder === 'asc'
? valA.localeCompare(valB)
: valB.localeCompare(valA);
}
return sortOrder === 'asc'
? (valA > valB ? 1 : -1)
: (valB > valA ? 1 : -1);
});
};
// 服务端排序处理
const handleServerSort = (field, order) => {
fetchData({
sortField: field,
sortOrder: order,
page: 1, // 排序后回到第一页
pageSize: pagination.pageSize,
filters
});
};
3.3 筛选功能实现
javascript
// 筛选逻辑(客户端筛选)
const getFilteredData = () => {
if (Object.keys(filters).length === 0) return data;
return data.filter(item => {
return Object.entries(filters).every(([field, value]) => {
if (!value) return true;
const column = columns.find(col => col.field === field);
if (!column) return true;
// 文本筛选
if (column.filterType === 'text') {
return String(item[field]).toLowerCase().includes(value.toLowerCase());
}
// 选择器筛选
if (column.filterType === 'select') {
return item[field] === value;
}
// 日期筛选
if (column.filterType === 'date') {
return new Date(item[field]).toDateString() === new Date(value).toDateString();
}
return true;
});
});
};
// 复合处理:筛选 -> 排序 -> 分页
const processedData = getPagedData(getSortedData(getFilteredData()));
四、高级功能扩展
4.1 自定义单元格渲染
jsx
const columns = [
{
field: 'status',
header: '状态',
render: (value) => (
<span className={`status-badge status-${value}`}>
{value === 'active' ? '活跃' : '禁用'}
</span>
)
},
{
field: 'actions',
header: '操作',
render: (_, record) => (
<div className="action-buttons">
<button onClick={() => handleEdit(record)}>编辑</button>
<button onClick={() => handleDelete(record)}>删除</button>
</div>
)
}
];
4.2 多选功能
jsx
const [selectedRows, setSelectedRows] = useState([]);
const toggleRowSelection = (rowId, checked) => {
setSelectedRows(prev =>
checked
? [...prev, rowId]
: prev.filter(id => id !== rowId)
);
};
const toggleAllSelection = (checked) => {
setSelectedRows(checked
? currentPageData.map(item => item[rowKey])
: []);
};
// 在表头添加全选复选框
<th>
<input
type="checkbox"
checked={selectedRows.length > 0 &&
selectedRows.length === currentPageData.length}
onChange={(e) => toggleAllSelection(e.target.checked)}
/>
</th>
// 在每行添加复选框
<td>
<input
type="checkbox"
checked={selectedRows.includes(record[rowKey])}
onChange={(e) => toggleRowSelection(record[rowKey], e.target.checked)}
/>
</td>
4.3 响应式设计
css
/* 基础表格样式 */
.table-container {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
/* 响应式处理 */
@media (max-width: 768px) {
/* 将表格转为卡片布局 */
table, thead, tbody, th, td, tr {
display: block;
}
thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
tr {
margin-bottom: 1rem;
border: 1px solid #ddd;
}
td {
border: none;
position: relative;
padding-left: 50%;
}
td:before {
position: absolute;
left: 1rem;
content: attr(data-label);
font-weight: bold;
}
}
/* 动态添加data-label属性 */
<td data-label={column.header}>
{column.render ? column.render(record[column.field], record) : record[column.field]}
</td>
五、性能优化方案
5.1 虚拟滚动(大数据量优化)
jsx
import { FixedSizeList as List } from 'react-window';
const VirtualizedTableBody = ({ data, columns, rowHeight }) => {
const Row = ({ index, style }) => (
<div style={style}>
{columns.map(column => (
<div key={column.field} style={{ width: column.width }}>
{column.render
? column.render(data[index][column.field], data[index])
: data[index][column.field]}
</div>
))}
</div>
);
return (
<List
height={500}
itemCount={data.length}
itemSize={rowHeight}
width="100%"
>
{Row}
</List>
);
};
5.2 数据缓存与记忆化
jsx
import { useMemo } from 'react';
const Table = ({ data, columns, /* other props */ }) => {
const processedData = useMemo(() => {
return getPagedData(getSortedData(getFilteredData(data)));
}, [data, sortField, sortOrder, filters, pagination]);
const memoizedColumns = useMemo(() => columns, [columns]);
// ...
};
5.3 按需渲染列
jsx
const [visibleColumns, setVisibleColumns] = useState(columns);
const toggleColumnVisibility = (field) => {
setVisibleColumns(prev =>
prev.some(col => col.field === field)
? prev.filter(col => col.field !== field)
: [...prev, columns.find(col => col.field === field)]
);
};
// 在表格顶部添加列选择器
<div className="column-selector">
{columns.map(column => (
<label key={column.field}>
<input
type="checkbox"
checked={visibleColumns.some(col => col.field === column.field)}
onChange={() => toggleColumnVisibility(column.field)}
/>
{column.header}
</label>
))}
</div>
六、完整实现流程图
graph TD
A[开始] --> B[初始化表格配置]
B --> C{是否服务端模式?}
C -->|是| D[监听参数变化请求数据]
C -->|否| E[处理本地数据]
E --> F[应用筛选条件]
F --> G[应用排序规则]
G --> H[应用分页逻辑]
H --> I[渲染表格]
I --> J[渲染表头]
J --> K[添加排序交互]
K --> L[添加筛选控件]
L --> M[渲染表体]
M --> N[自定义单元格渲染]
N --> O[渲染分页器]
O --> P[结束]
D --> Q[组合请求参数]
Q --> R[发送API请求]
R --> S[接收响应数据]
S --> I
七、组件使用示例
7.1 基础用法
jsx
const App = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
const columns = [
{ field: 'id', header: 'ID', width: 80 },
{ field: 'name', header: '姓名', sortable: true, filterType: 'text' },
{ field: 'age', header: '年龄', sortable: true },
{ field: 'email', header: '邮箱', filterType: 'text' },
{
field: 'status',
header: '状态',
filterType: 'select',
filterOptions: [
{ value: 'active', label: '活跃' },
{ value: 'inactive', label: '禁用' }
],
render: (value) => (
<span className={`status-${value}`}>
{value === 'active' ? '✔' : '✖'}
</span>
)
}
];
const fetchData = async (params = {}) => {
setLoading(true);
try {
const response = await api.get('/users', { params });
setData(response.data);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData({
page: pagination.current,
pageSize: pagination.pageSize
});
}, [pagination]);
const handlePageChange = (page, pageSize) => {
setPagination({ current: page, pageSize });
};
const handleSort = (field, order) => {
fetchData({
...pagination,
sortField: field,
sortOrder: order
});
};
const handleFilter = (filters) => {
fetchData({
...pagination,
...filters
});
};
return (
<Table
columns={columns}
data={data}
pagination={{
...pagination,
total: data.length
}}
loading={loading}
onPageChange={handlePageChange}
onSort={handleSort}
onFilter={handleFilter}
/>
);
};
7.2 服务端分页示例
jsx
const ServerPagedTable = () => {
const [state, setState] = useState({
data: [],
loading: false,
pagination: { current: 1, pageSize: 10, total: 0 },
filters: {},
sort: {}
});
const columns = [/* 列定义 */];
const fetchData = async () => {
setState(prev => ({ ...prev, loading: true }));
try {
const params = {
page: state.pagination.current,
pageSize: state.pagination.pageSize,
...state.filters,
...state.sort
};
const res = await api.get('/data', { params });
setState(prev => ({
...prev,
data: res.data.items,
pagination: {
...prev.pagination,
total: res.data.total
},
loading: false
}));
} catch (error) {
setState(prev => ({ ...prev, loading: false }));
}
};
useEffect(() => {
fetchData();
}, [state.pagination.current, state.pagination.pageSize, state.filters, state.sort]);
const handleTableChange = (pagination, filters, sort) => {
setState(prev => ({
...prev,
pagination: {
...prev.pagination,
current: pagination.current,
pageSize: pagination.pageSize
},
filters,
sort: {
field: sort.field,
order: sort.order
}
}));
};
return (
<Table
columns={columns}
data={state.data}
pagination={state.pagination}
loading={state.loading}
onPageChange={(page, size) => handleTableChange(
{ current: page, pageSize: size },
state.filters,
state.sort
)}
onSort={(field, order) => handleTableChange(
state.pagination,
state.filters,
{ field, order }
)}
onFilter={(filters) => handleTableChange(
state.pagination,
filters,
state.sort
)}
/>
);
};
八、测试方案
8.1 单元测试要点
javascript
describe('Table Component', () => {
it('正确渲染数据', () => {
const data = [{ id: 1, name: 'Alice' }];
const columns = [{ field: 'name', header: 'Name' }];
render(<Table columns={columns} data={data} />);
expect(screen.getByText('Alice')).toBeInTheDocument();
});
it('分页功能正常', () => {
const data = Array.from({ length: 25 }, (_, i) => ({ id: i + 1 }));
const { container } = render(
<Table
columns={[{ field: 'id', header: 'ID' }]}
data={data}
pagination={{ current: 1, pageSize: 10 }}
/>
);
// 第一页应显示10条数据
expect(container.querySelectorAll('tbody tr').length).toBe(10);
});
it('排序回调触发', () => {
const onSort = jest.fn();
render(
<Table
columns={[{ field: 'name', header: 'Name', sortable: true }]}
data={[{ id: 1, name: 'Alice' }]}
onSort={onSort}
/>
);
fireEvent.click(screen.getByText('Name'));
expect(onSort).toHaveBeenCalledWith('name', 'asc');
});
it('筛选功能正常', async () => {
const onFilter = jest.fn();
render(
<Table
columns={[
{
field: 'status',
header: 'Status',
filterType: 'select',
filterOptions: [{ value: 'active', label: 'Active' }]
}
]}
data={[{ id: 1, status: 'active' }]}
onFilter={onFilter}
/>
);
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'active' } });
await waitFor(() => {
expect(onFilter).toHaveBeenCalledWith({ status: 'active' });
});
});
});
九、总结与最佳实践
9.1 设计原则总结
- 单一职责:表格组件只负责展示和交互,数据获取由父组件控制
- 受控组件:通过props完全控制组件状态
- 可扩展性:通过render props支持自定义渲染
- 性能优先:虚拟滚动、记忆化等技术优化性能
9.2 最佳实践建议
- 服务端模式:大数据量时使用服务端分页、排序和筛选
- 按需加载:初始只加载可见区域数据
- 可访问性:添加ARIA属性支持屏幕阅读器
- 主题定制:通过CSS变量支持主题定制
- 文档完善:提供详细的API文档和使用示例
9.3 扩展方向
- 树形表格:支持嵌套数据展示
- 单元格合并:复杂表头支持
- 拖拽排序:行和列的拖拽调整
- 导出功能:支持导出CSV/Excel
- 图表集成:在单元格中嵌入微型图表
通过以上设计和实现,我们创建了一个功能强大、可复用性高的表格组件,能够满足大多数业务场景的需求,同时也为后续扩展留下了充足的空间。
