listTable添加跨页全选
parent
ef8195390f
commit
1bf8e9b211
|
|
@ -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<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}
|
||||
|
||||
// 分页配置 - 关键:自<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 支持跨页全选状态
|
||||
- 📝 完善文档和使用示例
|
||||
|
|
@ -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)
|
||||
- 🎉 初始版本
|
||||
- ✨ 基础表格功能
|
||||
- ✨ 行选择、分页、滚动支持
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
{/* 右侧搜索筛选区 */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -14,6 +14,11 @@
|
|||
"key": "button-designs",
|
||||
"label": "扩展按钮",
|
||||
"path": "/design/button-designs"
|
||||
},
|
||||
{
|
||||
"key": "cross-page-selection",
|
||||
"label": "跨页全选",
|
||||
"path": "/design/cross-page-selection"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue