635 lines
16 KiB
Markdown
635 lines
16 KiB
Markdown
# ListTable 组件
|
||
|
||
## 组件说明
|
||
|
||
列表表格组件,基于 Ant Design Table 组件封装,提供统一的表格样式、行选择、分页、滚动和行点击等功能。**支持跨页全选功能**。
|
||
|
||
## 组件位置
|
||
|
||
```
|
||
src/components/ListTable/ListTable.jsx
|
||
src/components/ListTable/ListTable.css
|
||
```
|
||
|
||
## 参数说明
|
||
|
||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||
|--------|------|------|--------|------|
|
||
| columns | Array<ColumnConfig> | 是 | - | 表格列配置数组 |
|
||
| dataSource | Array<Object> | 是 | - | 表格数据源 |
|
||
| rowKey | string | 否 | 'id' | 行数据的唯一标识字段名 |
|
||
| selectedRowKeys | Array<string\|number> | 否 | [] | 选中行的 key 数组 |
|
||
| onSelectionChange | function(keys: Array) | 否 | - | 行选择变化回调 |
|
||
| **isAllPagesSelected** | boolean | 否 | false | 是否跨页全选所有数据 |
|
||
| **totalCount** | number | 否 | - | 总数据量(用于跨页全选显示) |
|
||
| **onSelectAllPages** | function() | 否 | - | 选择所有页回调 |
|
||
| **onClearSelection** | function() | 否 | - | 清除选择回调 |
|
||
| pagination | Object\|false | 否 | 默认配置 | 分页配置,false 表示不分页 |
|
||
| scroll | Object | 否 | { x: 1200 } | 表格滚动配置 |
|
||
| onRowClick | function(record: Object) | 否 | - | 行点击回调 |
|
||
| selectedRow | Object | 否 | - | 当前选中的行数据对象 |
|
||
| loading | boolean | 否 | false | 表格加载状态 |
|
||
| className | string | 否 | '' | 自定义类名 |
|
||
|
||
### ColumnConfig 列配置
|
||
|
||
继承自 Ant Design Table 的列配置,常用属性:
|
||
|
||
| 属性名 | 类型 | 说明 |
|
||
|--------|------|------|
|
||
| title | string\|ReactNode | 列标题 |
|
||
| dataIndex | string | 数据字段名 |
|
||
| key | string | 列唯一标识 |
|
||
| width | number | 列宽度 |
|
||
| align | 'left'\|'center'\|'right' | 对齐方式 |
|
||
| fixed | 'left'\|'right' | 固定列 |
|
||
| render | function(value, record, index) | 自定义渲染函数 |
|
||
|
||
### 默认分页配置
|
||
|
||
```javascript
|
||
{
|
||
pageSize: 10,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
}
|
||
```
|
||
|
||
**注意**:当传入 `onSelectAllPages` 和 `onClearSelection` 时,组件会自动在 `showTotal` 位置显示选择信息和跨页全选操作:
|
||
|
||
- **无选中**:已选择 0 项
|
||
- **部分选中**:已选择 3 项 [选择全部 100 项] [清除]
|
||
- **跨页全选**:已选择 100 项 [清除选择]
|
||
|
||
## 使用示例
|
||
|
||
### 基础用法
|
||
|
||
```jsx
|
||
import ListTable from '../components/ListTable/ListTable'
|
||
|
||
function MyPage() {
|
||
const columns = [
|
||
{
|
||
title: '序号',
|
||
dataIndex: 'id',
|
||
key: 'id',
|
||
width: 80,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '用户名',
|
||
dataIndex: 'userName',
|
||
key: 'userName',
|
||
width: 150,
|
||
},
|
||
{
|
||
title: '姓名',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
width: 120,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
width: 100,
|
||
render: (status) => (
|
||
<Tag color={status === 'enabled' ? 'green' : 'default'}>
|
||
{status === 'enabled' ? '启用' : '停用'}
|
||
</Tag>
|
||
),
|
||
},
|
||
]
|
||
|
||
const dataSource = [
|
||
{ id: 1, userName: 'admin', name: '管理员', status: 'enabled' },
|
||
{ id: 2, userName: 'user', name: '张三', status: 'disabled' },
|
||
]
|
||
|
||
return (
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
/>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 带行选择
|
||
|
||
```jsx
|
||
import { useState } from 'react'
|
||
import ListTable from '../components/ListTable/ListTable'
|
||
|
||
function UserListPage() {
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||
|
||
return (
|
||
<div>
|
||
{/* 显示选中的数量 */}
|
||
<div>已选择 {selectedRowKeys.length} 项</div>
|
||
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
selectedRowKeys={selectedRowKeys}
|
||
onSelectionChange={setSelectedRowKeys}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 带行点击和高亮
|
||
|
||
```jsx
|
||
import { useState } from 'react'
|
||
import ListTable from '../components/ListTable/ListTable'
|
||
|
||
function UserListPage() {
|
||
const [selectedUser, setSelectedUser] = useState(null)
|
||
|
||
const handleRowClick = (record) => {
|
||
setSelectedUser(record)
|
||
// 打开详情抽屉等操作
|
||
setShowDetailDrawer(true)
|
||
}
|
||
|
||
return (
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
onRowClick={handleRowClick}
|
||
selectedRow={selectedUser}
|
||
/>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 自定义分页
|
||
|
||
```jsx
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
pagination={{
|
||
pageSize: 20,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (total) => `总计 ${total} 条记录`,
|
||
pageSizeOptions: ['10', '20', '50', '100'],
|
||
}}
|
||
/>
|
||
```
|
||
|
||
### 禁用分页
|
||
|
||
```jsx
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
pagination={false}
|
||
/>
|
||
```
|
||
|
||
### 带加载状态
|
||
|
||
```jsx
|
||
import { useState, useEffect } from 'react'
|
||
|
||
function UserListPage() {
|
||
const [loading, setLoading] = useState(false)
|
||
const [dataSource, setDataSource] = useState([])
|
||
|
||
const fetchData = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const data = await api.fetchUsers()
|
||
setDataSource(data)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchData()
|
||
}, [])
|
||
|
||
return (
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
loading={loading}
|
||
/>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 横向滚动(列较多时)
|
||
|
||
```jsx
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
scroll={{ x: 1600 }} // 内容宽度超过容器时出现横向滚动
|
||
/>
|
||
```
|
||
|
||
### 固定列
|
||
|
||
```jsx
|
||
const columns = [
|
||
{
|
||
title: '序号',
|
||
dataIndex: 'id',
|
||
key: 'id',
|
||
width: 80,
|
||
fixed: 'left', // 固定在左侧
|
||
},
|
||
// ... 其他列
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 200,
|
||
fixed: 'right', // 固定在右侧
|
||
render: (_, record) => (
|
||
<Space>
|
||
<Button type="link" size="small">编辑</Button>
|
||
<Button type="link" size="small" danger>删除</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
]
|
||
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={dataSource}
|
||
scroll={{ x: 1600 }}
|
||
/>
|
||
```
|
||
|
||
### 跨页全选(重要功能)
|
||
|
||
支持选择所有页的数据,而不仅仅是当前页。
|
||
|
||
```jsx
|
||
import { useState, useMemo } from 'react'
|
||
import ListTable from '../components/ListTable/ListTable'
|
||
|
||
function UserListPage() {
|
||
const [allData] = useState([...]) // 所有数据
|
||
const [currentPage, setCurrentPage] = useState(1)
|
||
const [pageSize, setPageSize] = useState(10)
|
||
|
||
// 选择状态
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||
const [isAllPagesSelected, setIsAllPagesSelected] = useState(false)
|
||
|
||
// 当前页数据
|
||
const currentPageData = useMemo(() => {
|
||
const start = (currentPage - 1) * pageSize
|
||
const end = start + pageSize
|
||
return allData.slice(start, end)
|
||
}, [allData, currentPage, pageSize])
|
||
|
||
// 选择变化处理(重要:支持跨页选择)
|
||
const handleSelectionChange = (keys) => {
|
||
// keys 是当前页面操作后的选中项
|
||
// 需要合并其他页的选中项,以保持跨页选择状态
|
||
|
||
const currentPageKeys = currentPageData.map(item => item.id)
|
||
const otherPagesSelectedKeys = selectedRowKeys.filter(
|
||
key => !currentPageKeys.includes(key)
|
||
)
|
||
const newSelectedKeys = [...otherPagesSelectedKeys, ...keys]
|
||
|
||
setSelectedRowKeys(newSelectedKeys)
|
||
setIsAllPagesSelected(false)
|
||
}
|
||
|
||
// 选择所有页
|
||
const handleSelectAllPages = () => {
|
||
setIsAllPagesSelected(true)
|
||
setSelectedRowKeys(allData.map(item => item.id))
|
||
}
|
||
|
||
// 清除选择
|
||
const handleClearSelection = () => {
|
||
setSelectedRowKeys([])
|
||
setIsAllPagesSelected(false)
|
||
}
|
||
|
||
return (
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={currentPageData}
|
||
selectedRowKeys={selectedRowKeys}
|
||
onSelectionChange={handleSelectionChange}
|
||
|
||
// 跨页全选相关 props
|
||
isAllPagesSelected={isAllPagesSelected}
|
||
totalCount={allData.length}
|
||
onSelectAllPages={handleSelectAllPages}
|
||
onClearSelection={handleClearSelection}
|
||
|
||
pagination={{
|
||
current: currentPage,
|
||
pageSize: pageSize,
|
||
total: allData.length,
|
||
onChange: (page, size) => {
|
||
setCurrentPage(page)
|
||
setPageSize(size)
|
||
},
|
||
}}
|
||
/>
|
||
)
|
||
}
|
||
```
|
||
|
||
**跨页选择的关键点**:
|
||
|
||
1. **状态管理**:需要维护 `selectedRowKeys` 和 `isAllPagesSelected` 两个状态
|
||
2. **选择合并**:在 `onSelectionChange` 中合并其他页的选择
|
||
3. **全选操作**:将所有数据的 key 设置到 `selectedRowKeys`
|
||
4. **传递回调**:传入 `onSelectAllPages` 和 `onClearSelection`
|
||
|
||
**工作原理**:
|
||
```javascript
|
||
// 第 1 页选中 row1, row2
|
||
selectedRowKeys = ['row1', 'row2']
|
||
|
||
// 切换到第 2 页,选中 row11
|
||
// handleSelectionChange 自动合并
|
||
selectedRowKeys = ['row1', 'row2', 'row11']
|
||
|
||
// 点击"选择全部"
|
||
isAllPagesSelected = true
|
||
selectedRowKeys = ['row1', 'row2', ..., 'row100']
|
||
```
|
||
|
||
### 完整示例(包含跨页全选)
|
||
|
||
```jsx
|
||
import { useState, useMemo } from 'react'
|
||
import { Button, Modal, message } from 'antd'
|
||
import ListTable from '../components/ListTable/ListTable'
|
||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||
import { DeleteOutlined, ExportOutlined } from '@ant-design/icons'
|
||
|
||
function UserListPage() {
|
||
const [allUsers] = useState([...]) // 所有用户数据
|
||
const [currentPage, setCurrentPage] = useState(1)
|
||
const [pageSize, setPageSize] = useState(10)
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||
const [isAllPagesSelected, setIsAllPagesSelected] = useState(false)
|
||
|
||
const currentPageData = useMemo(() => {
|
||
const start = (currentPage - 1) * pageSize
|
||
return allUsers.slice(start, start + pageSize)
|
||
}, [allUsers, currentPage, pageSize])
|
||
|
||
const handleSelectionChange = (keys) => {
|
||
const currentPageKeys = currentPageData.map(item => item.id)
|
||
const otherPagesSelectedKeys = selectedRowKeys.filter(
|
||
key => !currentPageKeys.includes(key)
|
||
)
|
||
setSelectedRowKeys([...otherPagesSelectedKeys, ...keys])
|
||
setIsAllPagesSelected(false)
|
||
}
|
||
|
||
const handleSelectAllPages = () => {
|
||
setIsAllPagesSelected(true)
|
||
setSelectedRowKeys(allUsers.map(item => item.id))
|
||
message.success(`已选择全部 ${allUsers.length} 条数据`)
|
||
}
|
||
|
||
const handleClearSelection = () => {
|
||
setSelectedRowKeys([])
|
||
setIsAllPagesSelected(false)
|
||
}
|
||
|
||
const handleBatchDelete = () => {
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: isAllPagesSelected
|
||
? `确定要删除全部 ${allUsers.length} 条数据吗?`
|
||
: `确定要删除选中的 ${selectedRowKeys.length} 条数据吗?`,
|
||
onOk: () => {
|
||
// 执行删除
|
||
message.success('删除成功')
|
||
handleClearSelection()
|
||
},
|
||
})
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
{/* 操作栏 */}
|
||
<ListActionBar
|
||
actions={[...]}
|
||
batchActions={[
|
||
{
|
||
key: 'delete',
|
||
label: '批量删除',
|
||
icon: <DeleteOutlined />,
|
||
danger: true,
|
||
disabled: selectedRowKeys.length === 0,
|
||
onClick: handleBatchDelete,
|
||
},
|
||
]}
|
||
search={...}
|
||
/>
|
||
|
||
{/* 表格 */}
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={currentPageData}
|
||
selectedRowKeys={selectedRowKeys}
|
||
onSelectionChange={handleSelectionChange}
|
||
isAllPagesSelected={isAllPagesSelected}
|
||
totalCount={allUsers.length}
|
||
onSelectAllPages={handleSelectAllPages}
|
||
onClearSelection={handleClearSelection}
|
||
pagination={{
|
||
current: currentPage,
|
||
pageSize: pageSize,
|
||
total: allUsers.length,
|
||
onChange: (page, size) => {
|
||
setCurrentPage(page)
|
||
setPageSize(size)
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 完整示例(不使用跨页全选)
|
||
|
||
```jsx
|
||
import { useState } from 'react'
|
||
import ListTable from '../components/ListTable/ListTable'
|
||
import { Tag, Button, Space } from 'antd'
|
||
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
||
|
||
function UserListPage() {
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||
const [selectedUser, setSelectedUser] = useState(null)
|
||
const [showDetailDrawer, setShowDetailDrawer] = useState(false)
|
||
|
||
const columns = [
|
||
{
|
||
title: '序号',
|
||
dataIndex: 'id',
|
||
key: 'id',
|
||
width: 80,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '用户名',
|
||
dataIndex: 'userName',
|
||
key: 'userName',
|
||
width: 150,
|
||
},
|
||
{
|
||
title: '姓名',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
width: 120,
|
||
},
|
||
{
|
||
title: '用户分组',
|
||
dataIndex: 'group',
|
||
key: 'group',
|
||
width: 150,
|
||
render: (text) => <Tag color="blue">{text}</Tag>,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
width: 100,
|
||
align: 'center',
|
||
render: (status) => (
|
||
<Tag color={status === 'enabled' ? 'green' : 'default'}>
|
||
{status === 'enabled' ? '启用' : '停用'}
|
||
</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 180,
|
||
fixed: 'right',
|
||
render: (_, record) => (
|
||
<Space size="small" onClick={(e) => e.stopPropagation()}>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<EditOutlined />}
|
||
onClick={() => handleEdit(record)}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<DeleteOutlined />}
|
||
danger
|
||
onClick={() => handleDelete(record)}
|
||
>
|
||
删除
|
||
</Button>
|
||
</Space>
|
||
),
|
||
},
|
||
]
|
||
|
||
const handleRowClick = (record) => {
|
||
setSelectedUser(record)
|
||
setShowDetailDrawer(true)
|
||
}
|
||
|
||
return (
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={filteredUsers}
|
||
selectedRowKeys={selectedRowKeys}
|
||
onSelectionChange={setSelectedRowKeys}
|
||
onRowClick={handleRowClick}
|
||
selectedRow={selectedUser}
|
||
scroll={{ x: 1400 }}
|
||
/>
|
||
)
|
||
}
|
||
```
|
||
|
||
## 样式定制
|
||
|
||
组件提供以下 CSS 类名供自定义样式:
|
||
|
||
- `.list-table-container` - 表格容器
|
||
- `.row-selected` - 选中行的类名
|
||
- `.table-selection-info` - 分页器中的选择信息容器
|
||
- `.selection-count` - 选择数量文本
|
||
- `.count-highlight` - 高亮数字
|
||
- `.selection-action` - 操作链接(选择全部、清除)
|
||
|
||
### 固定高度设计
|
||
|
||
ListTable 组件采用固定表格体高度设计,确保页面布局的稳定性:
|
||
|
||
- **尺寸**:使用 Ant Design `size="middle"` 属性
|
||
- **行高**:47px(Ant Design middle 尺寸默认值)
|
||
- **表格体高度**:470px(固定高度,内部滚动)
|
||
- **显示行数**:10 行(470px ÷ 47px = 10)
|
||
- **分页器高度**:56px(Ant Design 默认)
|
||
- **容器内边距**:16px × 2
|
||
|
||
**设计说明**:
|
||
- 组件使用 Ant Design 的标准 `size="middle"` 属性,不自定义行高
|
||
- 表格体固定 470px 高度,恰好显示 10 行数据
|
||
- 超过 10 行的内容通过表格体内部滚动查看
|
||
- 当数据不足 10 行时,表格体仍保持 470px 高度,确保布局稳定
|
||
- 分页器上边距 16px,与表格体保持适当间距
|
||
|
||
## 使用场景
|
||
|
||
1. **用户列表** - 显示和管理用户数据
|
||
2. **设备列表** - 显示和管理设备信息
|
||
3. **订单列表** - 显示订单数据
|
||
4. **任何需要表格展示的数据列表**
|
||
|
||
## 注意事项
|
||
|
||
1. `columns` 配置中的 `key` 必须唯一
|
||
2. `dataSource` 中的每条数据必须有 `rowKey` 指定的唯一标识字段(默认为 `id`)
|
||
3. 操作列中的点击事件需要使用 `e.stopPropagation()` 阻止事件冒泡,避免触发行点击
|
||
4. 当列数较多时,建议设置合适的 `scroll.x` 值并固定首尾列
|
||
5. `selectedRow` 用于高亮显示,`selectedRowKeys` 用于多选
|
||
6. 使用 `render` 函数时,要注意性能,避免在渲染函数中进行复杂计算
|
||
7. **跨页全选**:必须在 `onSelectionChange` 中合并其他页的选择,否则切换页面会丢失选择状态
|
||
8. **跨页全选**:传入 `onSelectAllPages` 和 `onClearSelection` 后,组件会自动显示选择信息和全选操作
|
||
9. **跨页全选**:`totalCount` 用于显示总数量,如果不传则使用 `pagination.total`
|
||
|
||
## 相关组件
|
||
|
||
- [ListActionBar](./ListActionBar.md) - 列表操作栏组件(支持批量操作)
|
||
- [CrossPageSelection](./CrossPageSelection.md) - 跨页全选功能完整文档
|
||
|
||
---
|
||
|
||
**更新日志**
|
||
|
||
- v1.1.0 (2025-11-18)
|
||
- ✨ 新增跨页全选功能支持
|
||
- ✨ 自动在分页器显示选择信息和全选操作
|
||
- 📝 完善文档和使用示例
|
||
|
||
- v1.0.0 (2025-11-15)
|
||
- 🎉 初始版本
|
||
- ✨ 基础表格功能
|
||
- ✨ 行选择、分页、滚动支持
|