nex_design/docs/components/CrossPageSelection.md

16 KiB
Raw Blame History

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 对象

{
  count: number,              // 选中数量
  total: number,              // 总数据量
  isAllPagesSelected: boolean // 是否跨页全选
}

Action 配置

{
  key: string,          // 唯一标识
  label: string,        // 按钮文本
  type?: string,        // 按钮类型default | primary | dashed | link | text
  icon?: ReactNode,     // 图标
  disabled?: boolean,   // 是否禁用
  danger?: boolean,     // 危险按钮样式
  onClick: function     // 点击回调
}

使用示例

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

import ListTable from '../components/ListTable/ListTable'

<ListTable
  columns={columns}
  dataSource={currentPageData}
  rowKey="id"
  selectedRowKeys={selectedRowKeys}
  onSelectionChange={handleSelectionChange}

  // 跨页全选相关
  isAllPagesSelected={isAllPagesSelected}
  totalCount={totalCount}

  // 分页配置 - 关键<EFBFBD><EFBFBD><EFBFBD> showTotal
  pagination={{
    current: currentPage,
    pageSize: pageSize,
    total: totalCount,
    showSizeChanger: true,
    showQuickJumper: true,
    showTotal: (total, range) => {
      // 如果有选中项,显示选择信息和全选操作
      if (selectedRowKeys.length > 0) {
        if (isAllPagesSelected) {
          return (
            <span className="pagination-selection-info">
              已选择全部 <strong>{total}</strong> 条数据
              <a onClick={handleClearSelection} style={{ marginLeft: 8 }}>
                清除选择
              </a>
            </span>
          )
        } else if (selectedRowKeys.length < total) {
          return (
            <span className="pagination-selection-info">
              已选择 <strong>{selectedRowKeys.length}</strong> 条数据
              <a onClick={handleSelectAllPages} style={{ marginLeft: 8 }}>
                选择全部 {total} 
              </a>
              <a onClick={handleClearSelection} style={{ marginLeft: 8 }}>
                清除
              </a>
            </span>
          )
        } else {
          return (
            <span className="pagination-selection-info">
              已选择全部 <strong>{total}</strong> 条数据
              <a onClick={handleClearSelection} style={{ marginLeft: 8 }}>
                清除选择
              </a>
            </span>
          )
        }
      }
      // 无选中项时,显示常规的分页信息
      return `共 ${total} 条`
    },
    onChange: (page, size) => {
      setCurrentPage(page)
      setPageSize(size)
    },
  }}
/>

分页器样式

/* 分页器中的选择信息样式 */
.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;
}

🚀 完整使用示例

状态管理

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])

  // ... 处理函数
}

核心处理函数

// 选择变化处理(重要:支持跨页选择)
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()
    },
  })
}

页面结构

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),需要手动维护跨页的选中状态:

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. 批量操作提示

执行批量操作时,明确提示操作范围:

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保持选择状态(推荐)

// 切换分页时,保持已选择的数据
const handlePageChange = (page, size) => {
  setCurrentPage(page)
  setPageSize(size)
  // 不清除 selectedRowKeys保持选择状态
}

策略 B清除选择状态

// 切换分页时,清除选择
const handlePageChange = (page, size) => {
  setCurrentPage(page)
  setPageSize(size)
  handleClearSelection() // 清除选择
}

3. 后端 API 对接

跨页全选时,后端 API 应支持两种方式:

// 方式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

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. 全选状态的一致性

    • 手动勾选/取消勾选时,应取消跨页全选状态
    • 确保 selectedRowKeysisAllPagesSelected 状态同步
  2. 筛选和搜索

    • 执行筛选或搜索后,应清除选择状态
    • 或者保持选择,但提示用户"选择的数据可能不在当前筛选结果中"
  3. 数据刷新

    • 刷新数据后,应重新评估选择状态的有效性
    • 建议清除选择,或检查已选数据是否仍然存在
  4. 权限控制

    • 批量操作应检查权限
    • 跨页全选时,应提示可能操作大量数据

🎨 样式定制

ListActionBar 批量操作区样式定制

/* 修改选中信息区域背景色 */
.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;
}

🔗 相关组件

📚 参考资料


更新日志

  • v1.2.0 (2025-11-18)

    • ♻️ 重构设计:将全选功能集成到操作栏,分页器只显示分页信息
    • 更清晰的职责分离:操作相关在操作栏,分页相关在分页器
    • 📝 更新文档和使用示例
  • v1.1.0 (2025-11-18)

    • ♻️ 重构设计:将全选功能集成到分页器,移除独立的 SelectionAlert 组件
    • 更简洁的交互:选择信息、全选操作、分页控制集中在一个区域
    • 📝 更新文档和使用示例
  • v1.0.0 (2025-11-18)

    • 新增 SelectionAlert 组件
    • 扩展 ListActionBar 支持批量操作
    • 扩展 ListTable 支持跨页全选状态
    • 📝 完善文档和使用示例