listTable添加跨页全选

main
mula.liu 2025-11-18 17:57:08 +08:00
parent ef8195390f
commit 1bf8e9b211
12 changed files with 1494 additions and 11 deletions

View File

@ -0,0 +1,590 @@
# 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&lt;Action&gt; | [] |
| 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}
// 分页配置 - 关键<EFBC9A><E887AA><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 支持跨页全选状态
- 📝 完善文档和使用示例

View File

@ -2,7 +2,7 @@
## 组件说明
列表表格组件,基于 Ant Design Table 组件封装,提供统一的表格样式、行选择、分页、滚动和行点击等功能。
列表表格组件,基于 Ant Design Table 组件封装,提供统一的表格样式、行选择、分页、滚动和行点击等功能。**支持跨页全选功能**。
## 组件位置
@ -20,6 +20,10 @@ src/components/ListTable/ListTable.css
| 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) | 否 | - | 行点击回调 |
@ -48,10 +52,15 @@ src/components/ListTable/ListTable.css
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
}
```
**注意**:当传入 `onSelectAllPages``onClearSelection` 时,组件会自动在 `showTotal` 位置显示选择信息和跨页全选操作:
- **无选中**:已选择 0 项
- **部分选中**:已选择 3 项 [选择全部 100 项] [清除]
- **跨页全选**:已选择 100 项 [清除选择]
## 使用示例
### 基础用法
@ -260,7 +269,204 @@ const columns = [
/>
```
### 完整示例
### 跨页全选(重要功能)
支持选择所有页的数据,而不仅仅是当前页。
```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'
@ -366,6 +572,10 @@ function UserListPage() {
- `.list-table-container` - 表格容器
- `.row-selected` - 选中行的类名
- `.table-selection-info` - 分页器中的选择信息容器
- `.selection-count` - 选择数量文本
- `.count-highlight` - 高亮数字
- `.selection-action` - 操作链接(选择全部、清除)
### 固定高度设计
@ -399,5 +609,26 @@ ListTable 组件采用固定表格体高度设计,确保页面布局的稳定
3. 操作列中的点击事件需要使用 `e.stopPropagation()` 阻止事件冒泡,避免触发行点击
4. 当列数较多时,建议设置合适的 `scroll.x` 值并固定首尾列
5. `selectedRow` 用于高亮显示,`selectedRowKeys` 用于多选
6. 分页的 `total` 值会自动根据 `dataSource.length` 计算
7. 使用 `render` 函数时,要注意性能,避免在渲染函数中进行复杂计算
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)
- 🎉 初始版本
- ✨ 基础表格功能
- ✨ 行选择、分页、滚动支持

View File

@ -7,6 +7,7 @@ import ImageListPage from './pages/ImageListPage'
import VirtualMachineImagePage from './pages/VirtualMachineImagePage'
import DocsPage from './pages/DocsPage'
import AllButtonDesigns from './pages/AllButtonDesigns'
import CrossPageSelectionDemo from './pages/CrossPageSelectionDemo'
function App() {
return (
@ -20,6 +21,7 @@ function App() {
<Route path="/image/vm" element={<VirtualMachineImagePage />} />
<Route path="/design" element={<DocsPage />} />
<Route path="/design/button-designs" element={<AllButtonDesigns />} />
<Route path="/design/cross-page-selection" element={<CrossPageSelectionDemo />} />
{/* 其他路由将在后续添加 */}
</Routes>
</MainLayout>

View File

@ -35,6 +35,51 @@
border-bottom-left-radius: 0;
}
/* 批量操作区域样式 */
.selection-info {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: #e6f4ff;
border: 1px solid #91caff;
border-radius: 6px;
font-size: 14px;
}
.selection-count {
color: rgba(0, 0, 0, 0.85);
}
.selection-count strong {
color: #1677ff;
font-weight: 600;
margin: 0 4px;
}
.all-pages-tag {
color: #1677ff;
font-weight: 500;
margin-left: 4px;
}
.select-all-link,
.clear-selection-link {
color: #1677ff;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
padding: 2px 8px;
border-radius: 4px;
transition: all 0.2s;
}
.select-all-link:hover,
.clear-selection-link:hover {
background: rgba(22, 119, 255, 0.1);
text-decoration: underline;
}
/* 响应式 */
@media (max-width: 768px) {
.list-action-bar {

View File

@ -8,6 +8,10 @@ const { Search } = Input
* 列表操作栏组件
* @param {Object} props
* @param {Array} props.actions - 左侧操作按钮配置数组
* @param {Array} props.batchActions - 批量操作按钮配置数组仅在有选中项时显示
* @param {Object} props.selectionInfo - 选中信息 { count: 选中数量, total: 总数量, isAllPagesSelected: 是否跨页全选 }
* @param {Function} props.onSelectAllPages - 选择所有页回调
* @param {Function} props.onClearSelection - 清除选择回调
* @param {Object} props.search - 搜索配置
* @param {Object} props.filter - 高级筛选配置可选
* @param {boolean} props.showRefresh - 是否显示刷新按钮
@ -15,16 +19,23 @@ const { Search } = Input
*/
function ListActionBar({
actions = [],
batchActions = [],
selectionInfo,
onSelectAllPages,
onClearSelection,
search,
filter,
showRefresh = false,
onRefresh,
}) {
//
const hasSelection = selectionInfo && selectionInfo.count > 0
return (
<div className="list-action-bar">
{/* 左侧操作按钮区 */}
<div className="list-action-bar-left">
{actions.map((action) => (
{/* 常规操作按钮(无选中时显示) */}
{!hasSelection && actions.map((action) => (
<Button
key={action.key}
type={action.type || 'default'}
@ -36,6 +47,43 @@ function ListActionBar({
{action.label}
</Button>
))}
{/* 批量操作区域(有选中时显示) */}
{hasSelection && (
<Space>
{/* 选中信息 */}
<div className="selection-info">
<span className="selection-count">
已选择 <strong>{selectionInfo.count}</strong>
{selectionInfo.isAllPagesSelected && (
<span className="all-pages-tag">全部页</span>
)}
</span>
{!selectionInfo.isAllPagesSelected && selectionInfo.total > selectionInfo.count && (
<a onClick={onSelectAllPages} className="select-all-link">
选择全部 {selectionInfo.total}
</a>
)}
<a onClick={onClearSelection} className="clear-selection-link">
清除
</a>
</div>
{/* 批量操作按钮 */}
{batchActions.map((action) => (
<Button
key={action.key}
type={action.type || 'default'}
icon={action.icon}
disabled={action.disabled}
danger={action.danger}
onClick={action.onClick}
>
{action.label}
</Button>
))}
</Space>
)}
</div>
{/* 右侧搜索筛选区 */}

View File

@ -8,3 +8,42 @@
overflow-y: auto;
width: 100%;
}
/* 行选中样式 */
.list-table-container .row-selected {
background-color: #e6f7ff;
}
.list-table-container .row-selected:hover > td {
background-color: #bae7ff !important;
}
/* 分页器中的选择信息样式 */
.table-selection-info {
display: inline-flex;
align-items: center;
gap: 8px;
}
.selection-count {
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
}
.count-highlight {
color: #1677ff;
font-weight: 600;
}
.selection-action {
color: #1677ff;
font-size: 14px;
cursor: pointer;
text-decoration: none;
transition: color 0.3s;
margin-left: 4px;
}
.selection-action:hover {
color: #4096ff;
}

View File

@ -9,6 +9,10 @@ import './ListTable.css'
* @param {string} props.rowKey - 行唯一标识字段
* @param {Array} props.selectedRowKeys - 选中的行
* @param {Function} props.onSelectionChange - 选择变化回调
* @param {boolean} props.isAllPagesSelected - 是否跨页全选所有数据
* @param {number} props.totalCount - 总数据量用于跨页全选
* @param {Function} props.onSelectAllPages - 选择所有页回调
* @param {Function} props.onClearSelection - 清除选择回调
* @param {Object} props.pagination - 分页配置
* @param {Object} props.scroll - 表格滚动配置
* @param {Function} props.onRowClick - 行点击回调
@ -22,11 +26,14 @@ function ListTable({
rowKey = 'id',
selectedRowKeys = [],
onSelectionChange,
isAllPagesSelected = false,
totalCount,
onSelectAllPages,
onClearSelection,
pagination = {
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
},
scroll = { x: 1200},
onRowClick,
@ -40,6 +47,49 @@ function ListTable({
onChange: (newSelectedRowKeys) => {
onSelectionChange?.(newSelectedRowKeys)
},
//
getCheckboxProps: () => ({
disabled: isAllPagesSelected,
}),
}
//
const mergedPagination = pagination === false ? false : {
...pagination,
showTotal: (total) => (
<div className="table-selection-info">
{isAllPagesSelected ? (
<>
<span className="selection-count">
已选择 <span className="count-highlight">{totalCount || total}</span>
</span>
{onClearSelection && (
<a onClick={onClearSelection} className="selection-action">
清除选择
</a>
)}
</>
) : selectedRowKeys.length > 0 ? (
<>
<span className="selection-count">
已选择 <span className="count-highlight">{selectedRowKeys.length}</span>
</span>
{onSelectAllPages && selectedRowKeys.length < (totalCount || total) && (
<a onClick={onSelectAllPages} className="selection-action">
选择全部 {totalCount || total}
</a>
)}
{onClearSelection && (
<a onClick={onClearSelection} className="selection-action">
清除
</a>
)}
</>
) : (
<span className="selection-count">已选择 0 </span>
)}
</div>
),
}
return (
@ -50,10 +100,7 @@ function ListTable({
columns={columns}
dataSource={dataSource}
rowKey={rowKey}
pagination={{
...pagination,
total: dataSource?.length || 0,
}}
pagination={mergedPagination}
scroll={scroll}
loading={loading}
onRow={(record) => ({

View File

@ -0,0 +1,49 @@
.selection-alert-container {
margin-bottom: 16px;
}
.selection-alert-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.selection-alert-content span {
flex: 1;
color: rgba(0, 0, 0, 0.85);
}
.selection-alert-content strong {
color: #1677ff;
font-weight: 600;
margin: 0 4px;
}
.selection-alert-content a {
color: #1677ff;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
text-decoration: none;
padding: 0 8px;
border-radius: 4px;
}
.selection-alert-content a:hover {
background: rgba(22, 119, 255, 0.08);
text-decoration: underline;
}
/* 响应式处理 */
@media (max-width: 768px) {
.selection-alert-content {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.selection-alert-content a {
padding: 4px 8px;
}
}

View File

@ -0,0 +1,89 @@
import { Alert } from 'antd'
import './SelectionAlert.css'
/**
* 全选提示条组件
* @param {Object} props
* @param {number} props.currentPageCount - 当前页选中数量
* @param {number} props.totalCount - 总数据量
* @param {boolean} props.isAllPagesSelected - 是否已选择所有页
* @param {Function} props.onSelectAllPages - 选择所有页的回调
* @param {Function} props.onClearSelection - 清除选择的回调
*/
function SelectionAlert({
currentPageCount,
totalCount,
isAllPagesSelected,
onSelectAllPages,
onClearSelection,
}) {
//
if (currentPageCount === 0) {
return null
}
//
if (isAllPagesSelected) {
return (
<div className="selection-alert-container">
<Alert
message={
<div className="selection-alert-content">
<span>
已选择全部 <strong>{totalCount}</strong> 条数据
</span>
<a onClick={onClearSelection}>清除选择</a>
</div>
}
type="info"
showIcon
closable={false}
/>
</div>
)
}
//
if (currentPageCount > 0 && totalCount > currentPageCount) {
return (
<div className="selection-alert-container">
<Alert
message={
<div className="selection-alert-content">
<span>
已选择当前页 <strong>{currentPageCount}</strong> 条数据
</span>
<a onClick={onSelectAllPages}>
选择全部 {totalCount} 条数据
</a>
</div>
}
type="warning"
showIcon
closable={false}
/>
</div>
)
}
//
return (
<div className="selection-alert-container">
<Alert
message={
<div className="selection-alert-content">
<span>
已选择 <strong>{currentPageCount}</strong> 条数据
</span>
<a onClick={onClearSelection}>清除选择</a>
</div>
}
type="info"
showIcon
closable={false}
/>
</div>
)
}
export default SelectionAlert

View File

@ -14,6 +14,11 @@
"key": "button-designs",
"label": "扩展按钮",
"path": "/design/button-designs"
},
{
"key": "cross-page-selection",
"label": "跨页全选",
"path": "/design/cross-page-selection"
}
]
},

View File

@ -0,0 +1,44 @@
.cross-page-selection-demo {
padding: 24px;
background: #f5f5f5;
min-height: 100vh;
}
.cross-page-selection-demo > * {
margin-bottom: 16px;
}
/* 使用说明样式 */
.demo-instructions {
background: #ffffff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-top: 24px;
}
.demo-instructions h3 {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.demo-instructions ol {
margin: 0;
padding-left: 20px;
color: rgba(0, 0, 0, 0.65);
line-height: 2;
}
.demo-instructions ol li {
margin-bottom: 8px;
}
.demo-instructions strong {
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
}

View File

@ -0,0 +1,294 @@
import { useState, useMemo } from 'react'
import { Button, Space, message, Modal } from 'antd'
import { PlusOutlined, DeleteOutlined, ExportOutlined, EditOutlined } from '@ant-design/icons'
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
import ListActionBar from '../components/ListActionBar/ListActionBar'
import ListTable from '../components/ListTable/ListTable'
import './CrossPageSelectionDemo.css'
//
const generateMockData = (count = 100) => {
return Array.from({ length: count }, (_, index) => ({
id: `terminal-${index + 1}`,
name: `终端设备 ${index + 1}`,
ip: `192.168.${Math.floor(index / 255)}.${(index % 255) + 1}`,
status: ['在线', '离线', '故障'][index % 3],
location: ['机房A', '机房B', '机房C', '机房D'][index % 4],
type: ['服务器', '交换机', '路由器'][index % 3],
updateTime: `2025-11-${String((index % 28) + 1).padStart(2, '0')} ${String((index % 24)).padStart(2, '0')}:00`,
}))
}
function CrossPageSelectionDemo() {
//
const mockData = useMemo(() => generateMockData(100), [])
//
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
//
const [selectedRowKeys, setSelectedRowKeys] = useState([])
const [isAllPagesSelected, setIsAllPagesSelected] = useState(false)
//
const [searchValue, setSearchValue] = useState('')
//
const currentPageData = useMemo(() => {
const start = (currentPage - 1) * pageSize
const end = start + pageSize
return mockData.slice(start, end)
}, [mockData, currentPage, pageSize])
//
const columns = [
{
title: '设备名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: 'IP地址',
dataIndex: 'ip',
key: 'ip',
width: 150,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status) => {
const colorMap = { '在线': 'green', '离线': 'gray', '故障': 'red' }
return (
<span style={{ color: colorMap[status] }}>
{status}
</span>
)
},
},
{
title: '位置',
dataIndex: 'location',
key: 'location',
width: 120,
},
{
title: '设备类型',
dataIndex: 'type',
key: 'type',
width: 120,
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: 180,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 120,
render: (_, record) => (
<Space size="small">
<a onClick={() => handleEdit(record)}>编辑</a>
<a onClick={() => handleView(record)}>查看</a>
</Space>
),
},
]
//
const handleSelectionChange = (keys) => {
// keys
// <EFBFBD><EFBFBD><EFBFBD>
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(mockData.map(item => item.id))
message.success(`已选择全部 ${mockData.length} 条数据`)
}
//
const handleClearSelection = () => {
setSelectedRowKeys([])
setIsAllPagesSelected(false)
message.info('已清除选择')
}
//
const handleBatchDelete = () => {
Modal.confirm({
title: '确认删除',
content: isAllPagesSelected
? `确定要删除全部 ${mockData.length} 条数据吗?`
: `确定要删除选中的 ${selectedRowKeys.length} 条数据吗?`,
okText: '确定',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: () => {
message.success(
isAllPagesSelected
? `成功删除 ${mockData.length} 条数据`
: `成功删除 ${selectedRowKeys.length} 条数据`
)
handleClearSelection()
},
})
}
//
const handleBatchExport = () => {
message.loading({
content: isAllPagesSelected
? `正在导出全部 ${mockData.length} 条数据...`
: `正在导出 ${selectedRowKeys.length} 条数据...`,
duration: 2,
})
setTimeout(() => {
message.success('导出成功!')
}, 2000)
}
//
const handleBatchEdit = () => {
message.info(
isAllPagesSelected
? `批量编辑全部 ${mockData.length} 条数据`
: `批量编辑 ${selectedRowKeys.length} 条数据`
)
}
//
const handleEdit = (record) => {
message.info(`编辑:${record.name}`)
}
//
const handleView = (record) => {
message.info(`查看:${record.name}`)
}
//
const handleRefresh = () => {
message.success('刷新成功')
}
//
const handleSearch = (value) => {
console.log('搜索:', value)
message.info(`搜索:${value}`)
}
return (
<div className="cross-page-selection-demo">
{/* 页面标题 */}
<PageTitleBar
title="跨页全选示例"
subtitle="演示列表跨页全选功能"
/>
{/* 操作栏 */}
<ListActionBar
//
actions={[
{
key: 'add',
label: '新建',
type: 'primary',
icon: <PlusOutlined />,
onClick: () => message.info('新建设备'),
},
]}
//
batchActions={[
{
key: 'edit',
label: '批量编辑',
icon: <EditOutlined />,
disabled: selectedRowKeys.length === 0,
onClick: handleBatchEdit,
},
{
key: 'export',
label: '批量导出',
icon: <ExportOutlined />,
disabled: selectedRowKeys.length === 0,
onClick: handleBatchExport,
},
{
key: 'delete',
label: '批量删除',
icon: <DeleteOutlined />,
danger: true,
disabled: selectedRowKeys.length === 0,
onClick: handleBatchDelete,
},
]}
//
search={{
placeholder: '搜索设备名称、IP地址',
value: searchValue,
onChange: setSearchValue,
onSearch: handleSearch,
}}
//
showRefresh
onRefresh={handleRefresh}
/>
{/* 列表表格 */}
<ListTable
columns={columns}
dataSource={currentPageData}
rowKey="id"
selectedRowKeys={selectedRowKeys}
onSelectionChange={handleSelectionChange}
isAllPagesSelected={isAllPagesSelected}
totalCount={mockData.length}
onSelectAllPages={handleSelectAllPages}
onClearSelection={handleClearSelection}
pagination={{
current: currentPage,
pageSize: pageSize,
total: mockData.length,
showSizeChanger: true,
showQuickJumper: true,
onChange: (page, size) => {
setCurrentPage(page)
setPageSize(size)
},
}}
/>
{/* 使用说明 */}
<div className="demo-instructions">
<h3>📖 使用说明</h3>
<ol>
<li><strong>当前页全选</strong>勾选表格左上角的全选框选择当前页的所有数据</li>
<li><strong>跨页全选</strong>选择部分数据后在表格底部左侧点击"选择全部 100 项"</li>
<li><strong>清除选择</strong>点击表格底部左侧的"清除"链接</li>
<li><strong>批量操作</strong>选中数据后操作栏会显示批量操作按钮批量编辑批量导出批量删除</li>
<li><strong>跨页保持</strong>在不同页面之间切换时选择状态会自动保持</li>
</ol>
</div>
</div>
)
}
export default CrossPageSelectionDemo