591 lines
16 KiB
Markdown
591 lines
16 KiB
Markdown
# CrossPageSelection - 跨页全选功能
|
||
|
||
## 📖 概述
|
||
|
||
跨页全选是列表页面中的常见需求,当数据量较大且分页显示时,用户可能需要选择所有数据而不仅仅是当前页的数据。本功能提供了一套完整的跨页全选解决方案,包括:
|
||
|
||
- **ListActionBar 扩展** - 选择信息显示和全选操作
|
||
- **ListTable 扩展** - 跨页全选状态支持
|
||
|
||
## 🎯 设计方案
|
||
|
||
### 设计理念
|
||
|
||
将选择信息和全选操作集成到操作栏(ListActionBar),分页器只显示分页信息,保持功能职责清晰分离。
|
||
|
||
### 交互流程
|
||
|
||
```
|
||
1. 用户勾选当前页或部分数据
|
||
↓
|
||
2. 操作栏显示:"已选择 5 项 [选择全部 100 项] [清除]"
|
||
↓
|
||
3. 用户点击"选择全部 100 项"
|
||
↓
|
||
4. 设置 isAllPagesSelected = true
|
||
↓
|
||
5. selectedRowKeys 包含所有数据的 key
|
||
↓
|
||
6. 操作栏更新:"已选择 100 项(全部页) [清除]"
|
||
↓
|
||
7. 执行批量操作(删除、导出等)
|
||
```
|
||
|
||
### 操作栏显示逻辑
|
||
|
||
| 选择状态 | 操作栏显示 |
|
||
|---------|-----------|
|
||
| 无选中 | [新建] 按钮 |
|
||
| 选中部分(< 总数) | "已选择 5 项 [选择全部 100 项] [清除]" + 批量操作按钮 |
|
||
| 跨页全选 | "已选择 100 项(全部页) [清除]" + 批量操作按钮 |
|
||
|
||
## 📦 核心功能
|
||
|
||
### 1. ListActionBar - 操作栏扩展(核心)
|
||
|
||
#### 新增功能
|
||
|
||
在原有 ListActionBar 的基础上,新增了批量操作区域:
|
||
|
||
- 当有选中项时,隐藏常规操作按钮,显示批量操作区域
|
||
- 显示选中信息(数量、是否跨页全选)
|
||
- 提供快捷的"选择全部"和"清除"链接
|
||
- 显示批量操作按钮(批量编辑、批量删除等)
|
||
|
||
#### 新增 Props
|
||
|
||
| 参数 | 说明 | 类型 | 默认值 |
|
||
|------|------|------|--------|
|
||
| batchActions | 批量操作按钮配置数组 | Array<Action> | [] |
|
||
| selectionInfo | 选中信息对象 | SelectionInfo | null |
|
||
| onSelectAllPages | 选择所有页回调 | function | - |
|
||
| onClearSelection | 清除选择回调 | function | - |
|
||
|
||
#### SelectionInfo 对象
|
||
|
||
```typescript
|
||
{
|
||
count: number, // 选中数量
|
||
total: number, // 总数据量
|
||
isAllPagesSelected: boolean // 是否跨页全选
|
||
}
|
||
```
|
||
|
||
#### Action 配置
|
||
|
||
```typescript
|
||
{
|
||
key: string, // 唯一标识
|
||
label: string, // 按钮文本
|
||
type?: string, // 按钮类型:default | primary | dashed | link | text
|
||
icon?: ReactNode, // 图标
|
||
disabled?: boolean, // 是否禁用
|
||
danger?: boolean, // 危险按钮样式
|
||
onClick: function // 点击回调
|
||
}
|
||
```
|
||
|
||
#### 使用示例
|
||
|
||
```jsx
|
||
import ListActionBar from '../components/ListActionBar/ListActionBar'
|
||
import { DeleteOutlined, ExportOutlined, EditOutlined } from '@ant-design/icons'
|
||
|
||
<ListActionBar
|
||
// 常规操作按钮(无选中时显示)
|
||
actions={[
|
||
{
|
||
key: 'add',
|
||
label: '新建',
|
||
type: 'primary',
|
||
icon: <PlusOutlined />,
|
||
onClick: handleAdd,
|
||
},
|
||
]}
|
||
|
||
// 批量操作按钮(有选中时显示)
|
||
batchActions={[
|
||
{
|
||
key: 'edit',
|
||
label: '批量编辑',
|
||
icon: <EditOutlined />,
|
||
onClick: handleBatchEdit,
|
||
},
|
||
{
|
||
key: 'export',
|
||
label: '批量导出',
|
||
icon: <ExportOutlined />,
|
||
onClick: handleBatchExport,
|
||
},
|
||
{
|
||
key: 'delete',
|
||
label: '批量删除',
|
||
icon: <DeleteOutlined />,
|
||
danger: true,
|
||
onClick: handleBatchDelete,
|
||
},
|
||
]}
|
||
|
||
// 选中信息
|
||
selectionInfo={
|
||
selectedRowKeys.length > 0
|
||
? {
|
||
count: selectedRowKeys.length,
|
||
total: totalCount,
|
||
isAllPagesSelected: isAllPagesSelected,
|
||
}
|
||
: null
|
||
}
|
||
onSelectAllPages={handleSelectAllPages}
|
||
onClearSelection={handleClearSelection}
|
||
|
||
// 其他原有配置...
|
||
search={{ ... }}
|
||
showRefresh
|
||
onRefresh={handleRefresh}
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
### 2. ListTable - 表格扩展(分页器集成全选)
|
||
|
||
#### 新增功能
|
||
|
||
支持跨页全选状态的显示:
|
||
|
||
- 当 `isAllPagesSelected=true` 时,所有行显示为选中状态
|
||
- 自定义表头全选框,显示"全选"标识
|
||
- 禁用单个 checkbox,防止用户取消选择(需通过"清除选择"统一取消)
|
||
|
||
#### 新增 Props
|
||
|
||
| 参数 | 说明 | 类型 | 默认值 |
|
||
|------|------|------|--------|
|
||
| isAllPagesSelected | 是否跨页全选所有数据 | boolean | false |
|
||
| totalCount | 总数据量 | number | - |
|
||
|
||
#### 使用示例(重点:自定义 showTotal)
|
||
|
||
```jsx
|
||
import ListTable from '../components/ListTable/ListTable'
|
||
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={currentPageData}
|
||
rowKey="id"
|
||
selectedRowKeys={selectedRowKeys}
|
||
onSelectionChange={handleSelectionChange}
|
||
|
||
// 跨页全选相关
|
||
isAllPagesSelected={isAllPagesSelected}
|
||
totalCount={totalCount}
|
||
|
||
// 分页配置 - 关键:自<EFBFBD><EFBFBD><EFBFBD>义 showTotal
|
||
pagination={{
|
||
current: currentPage,
|
||
pageSize: pageSize,
|
||
total: totalCount,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (total, range) => {
|
||
// 如果有选中项,显示选择信息和全选操作
|
||
if (selectedRowKeys.length > 0) {
|
||
if (isAllPagesSelected) {
|
||
return (
|
||
<span className="pagination-selection-info">
|
||
已选择全部 <strong>{total}</strong> 条数据
|
||
<a onClick={handleClearSelection} style={{ marginLeft: 8 }}>
|
||
清除选择
|
||
</a>
|
||
</span>
|
||
)
|
||
} else if (selectedRowKeys.length < total) {
|
||
return (
|
||
<span className="pagination-selection-info">
|
||
已选择 <strong>{selectedRowKeys.length}</strong> 条数据
|
||
<a onClick={handleSelectAllPages} style={{ marginLeft: 8 }}>
|
||
选择全部 {total} 条
|
||
</a>
|
||
<a onClick={handleClearSelection} style={{ marginLeft: 8 }}>
|
||
清除
|
||
</a>
|
||
</span>
|
||
)
|
||
} else {
|
||
return (
|
||
<span className="pagination-selection-info">
|
||
已选择全部 <strong>{total}</strong> 条数据
|
||
<a onClick={handleClearSelection} style={{ marginLeft: 8 }}>
|
||
清除选择
|
||
</a>
|
||
</span>
|
||
)
|
||
}
|
||
}
|
||
// 无选中项时,显示常规的分页信息
|
||
return `共 ${total} 条`
|
||
},
|
||
onChange: (page, size) => {
|
||
setCurrentPage(page)
|
||
setPageSize(size)
|
||
},
|
||
}}
|
||
/>
|
||
```
|
||
|
||
#### 分页器样式
|
||
|
||
```css
|
||
/* 分页器中的选择信息样式 */
|
||
.pagination-selection-info {
|
||
color: rgba(0, 0, 0, 0.85);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.pagination-selection-info strong {
|
||
color: #1677ff;
|
||
font-weight: 600;
|
||
margin: 0 4px;
|
||
}
|
||
|
||
.pagination-selection-info a {
|
||
color: #1677ff;
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.pagination-selection-info a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
```
|
||
|
||
## 🚀 完整使用示例
|
||
|
||
### 状态管理
|
||
|
||
```jsx
|
||
import { useState } from 'react'
|
||
|
||
function MyListPage() {
|
||
// 数据源(实际项目中通常从 API 获取)
|
||
const [dataSource] = useState([...]) // 所有数据
|
||
const [totalCount] = useState(dataSource.length)
|
||
|
||
// 分页状态
|
||
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 dataSource.slice(start, end)
|
||
}, [dataSource, currentPage, pageSize])
|
||
|
||
// ... 处理函数
|
||
}
|
||
```
|
||
|
||
### 核心处理函数
|
||
|
||
```jsx
|
||
// 选择变化处理(重要:支持跨页选择)
|
||
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(dataSource.map(item => item.id)) // 设置所有数据的 key
|
||
message.success(`已选择全部 ${totalCount} 条数据`)
|
||
}
|
||
|
||
// 清除选择
|
||
const handleClearSelection = () => {
|
||
setSelectedRowKeys([])
|
||
setIsAllPagesSelected(false)
|
||
message.info('已清除选择')
|
||
}
|
||
|
||
// 批量删除
|
||
const handleBatchDelete = () => {
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: isAllPagesSelected
|
||
? `确定要删除全部 ${totalCount} 条数据吗?`
|
||
: `确定要删除选中的 ${selectedRowKeys.length} 条数据吗?`,
|
||
onOk: () => {
|
||
// 执行删除逻辑
|
||
if (isAllPagesSelected) {
|
||
// 删除所有数据
|
||
} else {
|
||
// 删除选中的数据:selectedRowKeys
|
||
}
|
||
handleClearSelection()
|
||
},
|
||
})
|
||
}
|
||
```
|
||
|
||
### 页面结构
|
||
|
||
```jsx
|
||
return (
|
||
<div className="my-list-page">
|
||
{/* 操作栏 - 显示选择信息和全选操作 */}
|
||
<ListActionBar
|
||
actions={[...]}
|
||
batchActions={[...]}
|
||
selectionInfo={
|
||
selectedRowKeys.length > 0
|
||
? {
|
||
count: selectedRowKeys.length,
|
||
total: totalCount,
|
||
isAllPagesSelected,
|
||
}
|
||
: null
|
||
}
|
||
onSelectAllPages={handleSelectAllPages}
|
||
onClearSelection={handleClearSelection}
|
||
search={...}
|
||
/>
|
||
|
||
{/* 列表表格 - 分页器只显示分页信息 */}
|
||
<ListTable
|
||
columns={columns}
|
||
dataSource={currentPageData}
|
||
selectedRowKeys={selectedRowKeys}
|
||
onSelectionChange={handleSelectionChange}
|
||
isAllPagesSelected={isAllPagesSelected}
|
||
totalCount={totalCount}
|
||
pagination={{
|
||
showTotal: (total) => `共 ${total} 条`,
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
```
|
||
|
||
## 💡 最佳实践
|
||
|
||
### 0. 跨页选择的实现要点(重要!)
|
||
|
||
由于 Table 组件只接收当前页数据(`currentPageData`),需要手动维护跨页的选中状态:
|
||
|
||
```jsx
|
||
const handleSelectionChange = (keys) => {
|
||
// ❌ 错误做法:直接替换会丢失其他页的选择
|
||
// setSelectedRowKeys(keys)
|
||
|
||
// ✅ 正确做法:合并其他页的选择
|
||
const currentPageKeys = currentPageData.map(item => item.id)
|
||
const otherPagesSelectedKeys = selectedRowKeys.filter(key => !currentPageKeys.includes(key))
|
||
const newSelectedKeys = [...otherPagesSelectedKeys, ...keys]
|
||
|
||
setSelectedRowKeys(newSelectedKeys)
|
||
setIsAllPagesSelected(false)
|
||
}
|
||
```
|
||
|
||
**工作原理**:
|
||
1. `currentPageKeys` - 当前页所有数据的 key 列表
|
||
2. `otherPagesSelectedKeys` - 从已选项中过滤出不在当前页的 keys(即其他页的选择)
|
||
3. `newSelectedKeys` - 合并其他页的选择 + 当前页的新选择
|
||
4. 这样就保持了所有页的选中状态
|
||
|
||
### 1. 批量操作提示
|
||
|
||
执行批量操作时,明确提示操作范围:
|
||
|
||
```jsx
|
||
const handleBatchDelete = () => {
|
||
const count = isAllPagesSelected ? totalCount : selectedRowKeys.length
|
||
const scope = isAllPagesSelected ? '全部' : '选中的'
|
||
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: `确定要删除${scope} ${count} 条数据吗?此操作不可恢复。`,
|
||
okText: '确定',
|
||
cancelText: '取消',
|
||
okButtonProps: { danger: true },
|
||
onOk: async () => {
|
||
try {
|
||
if (isAllPagesSelected) {
|
||
await deleteAll()
|
||
} else {
|
||
await deleteByIds(selectedRowKeys)
|
||
}
|
||
message.success('删除成功')
|
||
handleClearSelection()
|
||
refreshData()
|
||
} catch (error) {
|
||
message.error('删除失败:' + error.message)
|
||
}
|
||
},
|
||
})
|
||
}
|
||
```
|
||
|
||
### 2. 分页切换时的选择状态
|
||
|
||
根据业务需求,可以选择两种策略:
|
||
|
||
**策略 A:保持选择状态**(推荐)
|
||
```jsx
|
||
// 切换分页时,保持已选择的数据
|
||
const handlePageChange = (page, size) => {
|
||
setCurrentPage(page)
|
||
setPageSize(size)
|
||
// 不清除 selectedRowKeys,保持选择状态
|
||
}
|
||
```
|
||
|
||
**策略 B:清除选择状态**
|
||
```jsx
|
||
// 切换分页时,清除选择
|
||
const handlePageChange = (page, size) => {
|
||
setCurrentPage(page)
|
||
setPageSize(size)
|
||
handleClearSelection() // 清除选择
|
||
}
|
||
```
|
||
|
||
### 3. 后端 API 对接
|
||
|
||
跨页全选时,后端 API 应支持两种方式:
|
||
|
||
```jsx
|
||
// 方式1:发送所有选中的 ID(推荐小数据量)
|
||
const handleBatchOperation = async () => {
|
||
await api.batchOperation({
|
||
ids: selectedRowKeys, // 所有选中的 ID
|
||
})
|
||
}
|
||
|
||
// 方式2:发送全选标识 + 筛选条件(推荐大数据量)
|
||
const handleBatchOperation = async () => {
|
||
await api.batchOperation({
|
||
selectAll: isAllPagesSelected,
|
||
filters: currentFilters, // 当前筛选条件
|
||
excludeIds: [], // 排除的 ID(如果有反选功能)
|
||
})
|
||
}
|
||
```
|
||
|
||
### 4. 性能优化
|
||
|
||
当数据量很大时(如超过 10000 条),避免一次性加载所有 ID:
|
||
|
||
```jsx
|
||
const handleSelectAllPages = () => {
|
||
setIsAllPagesSelected(true)
|
||
// 不要: setSelectedRowKeys(dataSource.map(item => item.id))
|
||
// 太多 ID 会导致性能问题
|
||
|
||
// 推荐: 只标记状态,实际操作时由后端根据筛选条件处理
|
||
message.success(`已选择全部数据`)
|
||
}
|
||
|
||
const handleBatchDelete = async () => {
|
||
if (isAllPagesSelected) {
|
||
// 发送全选标识和筛选条件,由后端处理
|
||
await api.deleteAll({ filters: currentFilters })
|
||
} else {
|
||
// 发送具体的 ID 列表
|
||
await api.deleteByIds(selectedRowKeys)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 📝 注意事项
|
||
|
||
1. **全选状态的一致性**
|
||
- 手动勾选/取消勾选时,应取消跨页全选状态
|
||
- 确保 `selectedRowKeys` 和 `isAllPagesSelected` 状态同步
|
||
|
||
2. **筛选和搜索**
|
||
- 执行筛选或搜索后,应清除选择状态
|
||
- 或者保持选择,但提示用户"选择的数据可能不在当前筛选结果中"
|
||
|
||
3. **数据刷新**
|
||
- 刷新数据后,应重新评估选择状态的有效性
|
||
- 建议清除选择,或检查已选数据是否仍然存在
|
||
|
||
4. **权限控制**
|
||
- 批量操作应检查权限
|
||
- 跨页全选时,应提示可能操作大量数据
|
||
|
||
## 🎨 样式定制
|
||
|
||
### ListActionBar 批量操作区样式定制
|
||
|
||
```css
|
||
/* 修改选中信息区域背景色 */
|
||
.selection-info {
|
||
background: #f0f9ff;
|
||
border-color: #69b1ff;
|
||
}
|
||
|
||
/* 修改选中数量颜色 */
|
||
.selection-count strong {
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
/* 修改全部页标识颜色 */
|
||
.all-pages-tag {
|
||
color: #ff4d4f;
|
||
}
|
||
|
||
/* 修改链接颜色 */
|
||
.select-all-link,
|
||
.clear-selection-link {
|
||
color: #52c41a;
|
||
}
|
||
```
|
||
|
||
## 🔗 相关组件
|
||
|
||
- [ListTable](./ListTable.md) - 列表表格组件
|
||
- [ListActionBar](./ListActionBar.md) - 列表<E58897><E8A1A8><EFBFBD>作栏组件
|
||
- [PageTitleBar](./PageTitleBar.md) - 页面标题栏组件
|
||
|
||
## 📚 参考资料
|
||
|
||
- [Ant Design Table 组件](https://ant.design/components/table-cn/)
|
||
- [Ant Design Pagination 组件](https://ant.design/components/pagination-cn/)
|
||
|
||
---
|
||
|
||
**更新日志**
|
||
|
||
- v1.2.0 (2025-11-18)
|
||
- ♻️ 重构设计:将全选功能集成到操作栏,分页器只显示分页信息
|
||
- ✨ 更清晰的职责分离:操作相关在操作栏,分页相关在分页器
|
||
- 📝 更新文档和使用示例
|
||
|
||
- v1.1.0 (2025-11-18)
|
||
- ♻️ 重构设计:将全选功能集成到分页器,移除独立的 SelectionAlert 组件
|
||
- ✨ 更简洁的交互:选择信息、全选操作、分页控制集中在一个区域
|
||
- 📝 更新文档和使用示例
|
||
|
||
- v1.0.0 (2025-11-18)
|
||
- ✨ 新增 SelectionAlert 组件
|
||
- ✨ 扩展 ListActionBar 支持批量操作
|
||
- ✨ 扩展 ListTable 支持跨页全选状态
|
||
- 📝 完善文档和使用示例
|