diff --git a/docs/components/CrossPageSelection.md b/docs/components/CrossPageSelection.md new file mode 100644 index 0000000..0fa4d52 --- /dev/null +++ b/docs/components/CrossPageSelection.md @@ -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' + +, + onClick: handleAdd, + }, + ]} + + // 批量操作按钮(有选中时显示) + batchActions={[ + { + key: 'edit', + label: '批量编辑', + icon: , + onClick: handleBatchEdit, + }, + { + key: 'export', + label: '批量导出', + icon: , + onClick: handleBatchExport, + }, + { + key: 'delete', + label: '批量删除', + icon: , + 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' + + { + // 如果有选中项,显示选择信息和全选操作 + if (selectedRowKeys.length > 0) { + if (isAllPagesSelected) { + return ( + + 已选择全部 {total} 条数据 + + 清除选择 + + + ) + } else if (selectedRowKeys.length < total) { + return ( + + 已选择 {selectedRowKeys.length} 条数据 + + 选择全部 {total} 条 + + + 清除 + + + ) + } else { + return ( + + 已选择全部 {total} 条数据 + + 清除选择 + + + ) + } + } + // 无选中项时,显示常规的分页信息 + 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 ( +
+ {/* 操作栏 - 显示选择信息和全选操作 */} + 0 + ? { + count: selectedRowKeys.length, + total: totalCount, + isAllPagesSelected, + } + : null + } + onSelectAllPages={handleSelectAllPages} + onClearSelection={handleClearSelection} + search={...} + /> + + {/* 列表表格 - 分页器只显示分页信息 */} + `共 ${total} 条`, + }} + /> +
+) +``` + +## 💡 最佳实践 + +### 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) - 列表���作栏组件 +- [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 支持跨页全选状态 + - 📝 完善文档和使用示例 diff --git a/docs/components/ListTable.md b/docs/components/ListTable.md index 0ebf2a5..cc95837 100644 --- a/docs/components/ListTable.md +++ b/docs/components/ListTable.md @@ -2,7 +2,7 @@ ## 组件说明 -列表表格组件,基于 Ant Design Table 组件封装,提供统一的表格样式、行选择、分页、滚动和行点击等功能。 +列表表格组件,基于 Ant Design Table 组件封装,提供统一的表格样式、行选择、分页、滚动和行点击等功能。**支持跨页全选功能**。 ## 组件位置 @@ -20,6 +20,10 @@ src/components/ListTable/ListTable.css | rowKey | string | 否 | 'id' | 行数据的唯一标识字段名 | | selectedRowKeys | Array | 否 | [] | 选中行的 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 ( + { + 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 ( +
+ {/* 操作栏 */} + , + danger: true, + disabled: selectedRowKeys.length === 0, + onClick: handleBatchDelete, + }, + ]} + search={...} + /> + + {/* 表格 */} + { + setCurrentPage(page) + setPageSize(size) + }, + }} + /> +
+ ) +} +``` + +### 完整示例(不使用跨页全选) ```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) + - 🎉 初始版本 + - ✨ 基础表格功能 + - ✨ 行选择、分页、滚动支持 diff --git a/src/App.jsx b/src/App.jsx index b4364c0..b895d55 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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() { } /> } /> } /> + } /> {/* 其他路由将在后续添加 */} diff --git a/src/components/ListActionBar/ListActionBar.css b/src/components/ListActionBar/ListActionBar.css index 0b60116..86b592f 100644 --- a/src/components/ListActionBar/ListActionBar.css +++ b/src/components/ListActionBar/ListActionBar.css @@ -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 { diff --git a/src/components/ListActionBar/ListActionBar.jsx b/src/components/ListActionBar/ListActionBar.jsx index 97c8043..f1b1c44 100644 --- a/src/components/ListActionBar/ListActionBar.jsx +++ b/src/components/ListActionBar/ListActionBar.jsx @@ -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 (
{/* 左侧操作按钮区 */}
- {actions.map((action) => ( + {/* 常规操作按钮(无选中时显示) */} + {!hasSelection && actions.map((action) => ( + ))} + + )}
{/* 右侧搜索筛选区 */} diff --git a/src/components/ListTable/ListTable.css b/src/components/ListTable/ListTable.css index 43e43f1..38deeee 100644 --- a/src/components/ListTable/ListTable.css +++ b/src/components/ListTable/ListTable.css @@ -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; +} diff --git a/src/components/ListTable/ListTable.jsx b/src/components/ListTable/ListTable.jsx index 69b90e5..77dd322 100644 --- a/src/components/ListTable/ListTable.jsx +++ b/src/components/ListTable/ListTable.jsx @@ -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) => ( +
+ {isAllPagesSelected ? ( + <> + + 已选择 {totalCount || total} 项 + + {onClearSelection && ( + + 清除选择 + + )} + + ) : selectedRowKeys.length > 0 ? ( + <> + + 已选择 {selectedRowKeys.length} 项 + + {onSelectAllPages && selectedRowKeys.length < (totalCount || total) && ( + + 选择全部 {totalCount || total} 项 + + )} + {onClearSelection && ( + + 清除 + + )} + + ) : ( + 已选择 0 项 + )} +
+ ), } 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) => ({ diff --git a/src/components/SelectionAlert/SelectionAlert.css b/src/components/SelectionAlert/SelectionAlert.css new file mode 100644 index 0000000..bfd2069 --- /dev/null +++ b/src/components/SelectionAlert/SelectionAlert.css @@ -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; + } +} diff --git a/src/components/SelectionAlert/SelectionAlert.jsx b/src/components/SelectionAlert/SelectionAlert.jsx new file mode 100644 index 0000000..82502cd --- /dev/null +++ b/src/components/SelectionAlert/SelectionAlert.jsx @@ -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 ( +
+ + + 已选择全部 {totalCount} 条数据 + + 清除选择 +
+ } + type="info" + showIcon + closable={false} + /> +
+ ) + } + + // 如果只选择了当前页,且总数大于当前页 + if (currentPageCount > 0 && totalCount > currentPageCount) { + return ( +
+ + + 已选择当前页 {currentPageCount} 条数据 + + + 选择全部 {totalCount} 条数据 + +
+ } + type="warning" + showIcon + closable={false} + /> + + ) + } + + // 只选择了部分数据,且总数等于当前页(单页情况) + return ( +
+ + + 已选择 {currentPageCount} 条数据 + + 清除选择 +
+ } + type="info" + showIcon + closable={false} + /> + + ) +} + +export default SelectionAlert diff --git a/src/data/menuData.json b/src/data/menuData.json index 1bfec6c..2cbb9d0 100644 --- a/src/data/menuData.json +++ b/src/data/menuData.json @@ -14,6 +14,11 @@ "key": "button-designs", "label": "扩展按钮", "path": "/design/button-designs" + }, + { + "key": "cross-page-selection", + "label": "跨页全选", + "path": "/design/cross-page-selection" } ] }, diff --git a/src/pages/CrossPageSelectionDemo.css b/src/pages/CrossPageSelectionDemo.css new file mode 100644 index 0000000..3d08a7b --- /dev/null +++ b/src/pages/CrossPageSelectionDemo.css @@ -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; +} diff --git a/src/pages/CrossPageSelectionDemo.jsx b/src/pages/CrossPageSelectionDemo.jsx new file mode 100644 index 0000000..94166b9 --- /dev/null +++ b/src/pages/CrossPageSelectionDemo.jsx @@ -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 ( + + ● {status} + + ) + }, + }, + { + 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) => ( + + handleEdit(record)}>编辑 + handleView(record)}>查看 + + ), + }, + ] + + // 选择变化处理 + 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(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 ( +
+ {/* 页面标题 */} + + + {/* 操作栏 */} + , + onClick: () => message.info('新建设备'), + }, + ]} + + // 批量操作按钮(始终显示,无选中时禁用) + batchActions={[ + { + key: 'edit', + label: '批量编辑', + icon: , + disabled: selectedRowKeys.length === 0, + onClick: handleBatchEdit, + }, + { + key: 'export', + label: '批量导出', + icon: , + disabled: selectedRowKeys.length === 0, + onClick: handleBatchExport, + }, + { + key: 'delete', + label: '批量删除', + icon: , + danger: true, + disabled: selectedRowKeys.length === 0, + onClick: handleBatchDelete, + }, + ]} + + // 搜索配置 + search={{ + placeholder: '搜索设备名称、IP地址', + value: searchValue, + onChange: setSearchValue, + onSearch: handleSearch, + }} + + // 刷新按钮 + showRefresh + onRefresh={handleRefresh} + /> + + {/* 列表表格 */} + { + setCurrentPage(page) + setPageSize(size) + }, + }} + /> + + {/* 使用说明 */} +
+

📖 使用说明

+
    +
  1. 当前页全选:勾选表格左上角的全选框,选择当前页的所有数据
  2. +
  3. 跨页全选:选择部分数据后,在表格底部左侧点击"选择全部 100 项"
  4. +
  5. 清除选择:点击表格底部左侧的"清除"链接
  6. +
  7. 批量操作:选中数据后,操作栏会显示批量操作按钮(批量编辑、批量导出、批量删除)
  8. +
  9. 跨页保持:在不同页面之间切换时,选择状态会自动保持
  10. +
+
+
+ ) +} + +export default CrossPageSelectionDemo