bulk-select-actions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

When to Use This Skill

何时使用该技能

Use when:
  • Building a table/list with multi-select functionality
  • Implementing bulk actions (delete, archive, export, status change)
  • Need a floating action toolbar that appears on selection
  • Want consistent selection UX across multiple tables
使用场景:
  • 构建具备多选功能的表格/列表
  • 实现批量操作(删除、归档、导出、状态变更)
  • 需要在选择时显示浮动操作工具栏
  • 希望在多个表格中保持一致的选择用户体验

Tech Stack

技术栈

PackageVersionPurpose
@radix-ui/react-checkbox
^1.xCheckbox with indeterminate state
tailwindcss-animate
^1.xEntrance animations
shadcn/ui
latestAlertDialog, Button, DropdownMenu
lucide-react
^0.xIcons
版本用途
@radix-ui/react-checkbox
^1.x支持不确定状态的复选框
tailwindcss-animate
^1.x入场动画
shadcn/ui
latest提示对话框、按钮、下拉菜单
lucide-react
^0.x图标库

Architecture Overview

架构概述

┌─────────────────────────────────────────────────────────────┐
│  Page Component (selection state owner)                     │
│  ├── selectedIds: Set<string>                               │
│  ├── onSelectionChange: (ids: Set<string>) => void          │
│  └── bulk action handlers                                   │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Table Component                                      │    │
│  │ ┌─────┬─────────────────────────────────────────┐   │    │
│  │ │ ☑️  │ Header row with select-all checkbox      │   │    │
│  │ ├─────┼─────────────────────────────────────────┤   │    │
│  │ │ ☐  │ Row 1                                    │   │    │
│  │ │ ☑️  │ Row 2 (selected)                         │   │    │
│  │ │ ☑️  │ Row 3 (selected)                         │   │    │
│  │ │ ☐  │ Row 4                                    │   │    │
│  │ └─────┴─────────────────────────────────────────┘   │    │
│  └─────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Floating Toolbar (fixed bottom center, z-50)        │    │
│  │ ┌──────────────┬────────────┬────────────┬───────┐  │    │
│  │ │ 2 selected ✕ │  Action 1  │  Action 2  │ Delete│  │    │
│  │ └──────────────┴────────────┴────────────┴───────┘  │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│  Page Component (selection state owner)                     │
│  ├── selectedIds: Set<string>                               │
│  ├── onSelectionChange: (ids: Set<string>) => void          │
│  └── bulk action handlers                                   │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Table Component                                      │    │
│  │ ┌─────┬─────────────────────────────────────────┐   │    │
│  │ │ ☑️  │ Header row with select-all checkbox      │   │    │
│  │ ├─────┼─────────────────────────────────────────┤   │    │
│  │ │ ☐  │ Row 1                                    │   │    │
│  │ │ ☑️  │ Row 2 (selected)                         │   │    │
│  │ │ ☑️  │ Row 3 (selected)                         │   │    │
│  │ │ ☐  │ Row 4                                    │   │    │
│  │ └─────┴─────────────────────────────────────────┘   │    │
│  └─────────────────────────────────────────────────────┘    │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Floating Toolbar (fixed bottom center, z-50)        │    │
│  │ ┌──────────────┬────────────┬────────────┬───────┐  │    │
│  │ │ 2 selected ✕ │  Action 1  │  Action 2  │ Delete│  │    │
│  │ └──────────────┴────────────┴────────────┴───────┘  │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Critical Patterns

核心实现模式

1. Selection State Management (Page Level)

1. 选择状态管理(页面层级)

Selection state MUST live in the page component, not the table:
typescript
// page.tsx
"use client";

import { useState } from "react";

export default function ItemsPage() {
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

  // Clear selection when filters change
  const handleFilterChange = (value: string) => {
    setFilter(value);
    setSelectedIds(new Set());
  };

  return (
    <>
      <ItemsTable
        items={items}
        selectedIds={selectedIds}
        onSelectionChange={setSelectedIds}
      />
      <BulkActionsToolbar
        selectedIds={selectedIds}
        onClearSelection={() => setSelectedIds(new Set())}
        // ... action handlers
      />
    </>
  );
}
选择状态必须存放在页面组件中,而非表格组件:
typescript
// page.tsx
"use client";

import { useState } from "react";

export default function ItemsPage() {
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

  // 筛选条件变更时清空选择
  const handleFilterChange = (value: string) => {
    setFilter(value);
    setSelectedIds(new Set());
  };

  return (
    <>
      <ItemsTable
        items={items}
        selectedIds={selectedIds}
        onSelectionChange={setSelectedIds}
      />
      <BulkActionsToolbar
        selectedIds={selectedIds}
        onClearSelection={() => setSelectedIds(new Set())}
        // ... 操作处理函数
      />
    </>
  );
}

2. Checkbox Indeterminate State (CRITICAL)

2. 复选框不确定状态(至关重要)

Use the built-in
checked
prop with
"indeterminate"
value:
typescript
// ✅ CORRECT - Use built-in indeterminate prop
<Checkbox
  checked={allSelected ? true : someSelected ? "indeterminate" : false}
  onCheckedChange={handleSelectAll}
  aria-label="Select all"
/>

// ❌ WRONG - Manual ref approach (gets overwritten by Radix)
<Checkbox
  checked={allSelected}
  ref={(el) => {
    if (el) {
      (el as HTMLButtonElement).dataset.state = someSelected
        ? "indeterminate"
        : "checked";
    }
  }}
/>
使用内置的
checked
属性并设置
"indeterminate"
值:
typescript
// ✅ 正确用法 - 使用内置的不确定状态属性
<Checkbox
  checked={allSelected ? true : someSelected ? "indeterminate" : false}
  onCheckedChange={handleSelectAll}
  aria-label="Select all"
/>

// ❌ 错误用法 - 手动操作ref(会被Radix覆盖)
<Checkbox
  checked={allSelected}
  ref={(el) => {
    if (el) {
      (el as HTMLButtonElement).dataset.state = someSelected
        ? "indeterminate"
        : "checked";
    }
  }}
/>

3. Selection Logic

3. 选择逻辑

typescript
// Table component
const allSelected = items.length > 0 && items.every((item) => selectedIds.has(item.id));
const someSelected = items.some((item) => selectedIds.has(item.id)) && !allSelected;

const handleSelectAll = () => {
  if (allSelected) {
    onSelectionChange(new Set()); // Deselect all
  } else {
    onSelectionChange(new Set(items.map((item) => item.id))); // Select all
  }
};

const handleSelectOne = (id: string, checked: boolean) => {
  const newSet = new Set(selectedIds);
  if (checked) {
    newSet.add(id);
  } else {
    newSet.delete(id);
  }
  onSelectionChange(newSet);
};
typescript
// 表格组件
const allSelected = items.length > 0 && items.every((item) => selectedIds.has(item.id));
const someSelected = items.some((item) => selectedIds.has(item.id)) && !allSelected;

const handleSelectAll = () => {
  if (allSelected) {
    onSelectionChange(new Set()); // 取消全选
  } else {
    onSelectionChange(new Set(items.map((item) => item.id))); // 全选
  }
};

const handleSelectOne = (id: string, checked: boolean) => {
  const newSet = new Set(selectedIds);
  if (checked) {
    newSet.add(id);
  } else {
    newSet.delete(id);
  }
  onSelectionChange(newSet);
};

4. Floating Toolbar Position & Animation

4. 浮动工具栏位置与动画

typescript
// CRITICAL: These exact classes for consistent UX
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-4 duration-200">
  <div className="flex items-center gap-2 bg-background border rounded-lg shadow-lg px-4 py-3">
    {/* Toolbar content */}
  </div>
</div>
typescript
// 关键:使用这些固定类名以保证一致的用户体验
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-4 duration-200">
  <div className="flex items-center gap-2 bg-background border rounded-lg shadow-lg px-4 py-3">
    {/* 工具栏内容 */}
  </div>
</div>

5. Toolbar Structure

5. 工具栏结构

typescript
export function BulkActionsToolbar({
  selectedIds,
  onClearSelection,
  onAction1,
  onAction2,
  onDelete,
  isLoading = false,
}: BulkActionsToolbarProps) {
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const selectedCount = selectedIds.size;

  // Hide when nothing selected
  if (selectedCount === 0) {
    return null;
  }

  return (
    <>
      <div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-4 duration-200">
        <div className="flex items-center gap-2 bg-background border rounded-lg shadow-lg px-4 py-3">
          {/* Selected count with clear button */}
          <div className="flex items-center gap-2 pr-3 border-r">
            <span className="text-sm font-medium">
              {selectedCount} selected
            </span>
            <Button
              variant="ghost"
              size="sm"
              className="h-6 w-6 p-0"
              onClick={onClearSelection}
            >
              <X className="h-4 w-4" />
            </Button>
          </div>

          {/* Action buttons */}
          <Button variant="outline" size="sm" onClick={onAction1} disabled={isLoading}>
            <Icon className="h-4 w-4 mr-2" />
            Action 1
          </Button>

          {/* Destructive action - always last, with confirmation */}
          <Button
            variant="outline"
            size="sm"
            onClick={() => setDeleteDialogOpen(true)}
            disabled={isLoading}
            className="text-red-600 hover:text-red-700 hover:bg-red-50"
          >
            <Trash2 className="h-4 w-4 mr-2" />
            Delete
          </Button>
        </div>
      </div>

      {/* Confirmation dialog for destructive actions */}
      <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Delete Items</AlertDialogTitle>
            <AlertDialogDescription>
              Are you sure you want to delete {selectedCount} item
              {selectedCount === 1 ? "" : "s"}? This action cannot be undone.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              onClick={() => { onDelete(); setDeleteDialogOpen(false); }}
              className="bg-red-600 hover:bg-red-700"
            >
              Delete {selectedCount} Item{selectedCount === 1 ? "" : "s"}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}
typescript
export function BulkActionsToolbar({
  selectedIds,
  onClearSelection,
  onAction1,
  onAction2,
  onDelete,
  isLoading = false,
}: BulkActionsToolbarProps) {
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const selectedCount = selectedIds.size;

  // 未选择任何内容时隐藏
  if (selectedCount === 0) {
    return null;
  }

  return (
    <>
      <div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-4 duration-200">
        <div className="flex items-center gap-2 bg-background border rounded-lg shadow-lg px-4 py-3">
          {/* 已选数量与清空按钮 */}
          <div className="flex items-center gap-2 pr-3 border-r">
            <span className="text-sm font-medium">
              {selectedCount} selected
            </span>
            <Button
              variant="ghost"
              size="sm"
              className="h-6 w-6 p-0"
              onClick={onClearSelection}
            >
              <X className="h-4 w-4" />
            </Button>
          </div>

          {/* 操作按钮 */}
          <Button variant="outline" size="sm" onClick={onAction1} disabled={isLoading}>
            <Icon className="h-4 w-4 mr-2" />
            Action 1
          </Button>

          {/* 破坏性操作 - 始终放在最后,并添加确认步骤 */}
          <Button
            variant="outline"
            size="sm"
            onClick={() => setDeleteDialogOpen(true)}
            disabled={isLoading}
            className="text-red-600 hover:text-red-700 hover:bg-red-50"
          >
            <Trash2 className="h-4 w-4 mr-2" />
            Delete
          </Button>
        </div>
      </div>

      {/* 破坏性操作的确认对话框 */}
      <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Delete Items</AlertDialogTitle>
            <AlertDialogDescription>
              Are you sure you want to delete {selectedCount} item
              {selectedCount === 1 ? "" : "s"}? This action cannot be undone.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              onClick={() => { onDelete(); setDeleteDialogOpen(false); }}
              className="bg-red-600 hover:bg-red-700"
            >
              Delete {selectedCount} Item{selectedCount === 1 ? "" : "s"}
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}

6. Row Checkbox - Prevent Row Click

6. 行内复选框 - 阻止行点击事件冒泡

typescript
<TableRow
  className="cursor-pointer"
  onClick={() => onRowClick(item.id)}
>
  <TableCell onClick={(e) => e.stopPropagation()}>
    <Checkbox
      checked={selectedIds.has(item.id)}
      onCheckedChange={(checked) => handleSelectOne(item.id, !!checked)}
      aria-label={`Select ${item.name}`}
    />
  </TableCell>
  {/* ... other cells */}
</TableRow>
typescript
<TableRow
  className="cursor-pointer"
  onClick={() => onRowClick(item.id)}
>
  <TableCell onClick={(e) => e.stopPropagation()}>
    <Checkbox
      checked={selectedIds.has(item.id)}
      onCheckedChange={(checked) => handleSelectOne(item.id, !!checked)}
      aria-label={`Select ${item.name}`}
    />
  </TableCell>
  {/* ... 其他单元格 */}
</TableRow>

7. Conditional Actions Based on Item State

7. 根据项目状态显示条件操作

typescript
// Check selected items' states for conditional actions
const selectedItems = items.filter((item) => selectedIds.has(item.id));
const allDraft = selectedItems.every((item) => item.status === "DRAFT");
const allPublished = selectedItems.every((item) => item.status === "PUBLISHED");

// Only show "Publish" if all selected are DRAFT
{allDraft && (
  <Button variant="outline" size="sm" onClick={onPublish}>
    <Globe className="h-4 w-4 mr-2" />
    Publish
  </Button>
)}

// Only show "Close" if all selected are PUBLISHED
{allPublished && (
  <Button variant="outline" size="sm" onClick={onClose}>
    <XCircle className="h-4 w-4 mr-2" />
    Close
  </Button>
)}
typescript
// 检查已选项目的状态以显示条件操作
const selectedItems = items.filter((item) => selectedIds.has(item.id));
const allDraft = selectedItems.every((item) => item.status === "DRAFT");
const allPublished = selectedItems.every((item) => item.status === "PUBLISHED");

// 仅当所有已选项目均为草稿时显示“发布”按钮
{allDraft && (
  <Button variant="outline" size="sm" onClick={onPublish}>
    <Globe className="h-4 w-4 mr-2" />
    Publish
  </Button>
)}

// 仅当所有已选项目均已发布时显示“关闭”按钮
{allPublished && (
  <Button variant="outline" size="sm" onClick={onClose}>
    <XCircle className="h-4 w-4 mr-2" />
    Close
  </Button>
)}

Table Component Props Interface

表格组件属性接口

typescript
interface SelectableTableProps<T extends { id: string }> {
  items: T[];
  isLoading?: boolean;
  selectedIds: Set<string>;
  onSelectionChange: (ids: Set<string>) => void;
  onRowClick?: (id: string) => void;
}
typescript
interface SelectableTableProps<T extends { id: string }> {
  items: T[];
  isLoading?: boolean;
  selectedIds: Set<string>;
  onSelectionChange: (ids: Set<string>) => void;
  onRowClick?: (id: string) => void;
}

Toolbar Component Props Interface

工具栏组件属性接口

typescript
interface BulkActionsToolbarProps {
  selectedIds: Set<string>;
  onClearSelection: () => void;
  isLoading?: boolean;
  // Add specific action handlers as needed
}
typescript
interface BulkActionsToolbarProps {
  selectedIds: Set<string>;
  onClearSelection: () => void;
  isLoading?: boolean;
  // 根据需要添加特定的操作处理函数
}

Styling Patterns

样式模式

Checkbox Column Width

复选框列宽度

typescript
<TableHead className="w-12">
  <Checkbox ... />
</TableHead>
typescript
<TableHead className="w-12">
  <Checkbox ... />
</TableHead>

Clear Selection Button

清空选择按钮

typescript
<Button
  variant="ghost"
  size="sm"
  className="h-6 w-6 p-0"  // Compact square button
  onClick={onClearSelection}
>
  <X className="h-4 w-4" />
</Button>
typescript
<Button
  variant="ghost"
  size="sm"
  className="h-6 w-6 p-0"  // 紧凑方形按钮
  onClick={onClearSelection}
>
  <X className="h-4 w-4" />
</Button>

Destructive Action Button

破坏性操作按钮

typescript
<Button
  variant="outline"
  size="sm"
  className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
  <Trash2 className="h-4 w-4 mr-2" />
  Delete
</Button>
typescript
<Button
  variant="outline"
  size="sm"
  className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
  <Trash2 className="h-4 w-4 mr-2" />
  Delete
</Button>

Confirmation Dialog Action

确认对话框操作按钮

typescript
<AlertDialogAction className="bg-red-600 hover:bg-red-700">
  Delete {count} Item{count === 1 ? "" : "s"}
</AlertDialogAction>
typescript
<AlertDialogAction className="bg-red-600 hover:bg-red-700">
  Delete {count} Item{count === 1 ? "" : "s"}
</AlertDialogAction>

Selected Count Section (with separator)

已选数量区域(带分隔线)

typescript
<div className="flex items-center gap-2 pr-3 border-r">
  <span className="text-sm font-medium">{count} selected</span>
  <ClearButton />
</div>
typescript
<div className="flex items-center gap-2 pr-3 border-r">
  <span className="text-sm font-medium">{count} selected</span>
  <ClearButton />
</div>

Common Mistakes to Avoid

需避免的常见错误

❌ Using useEffect to clear selection on filter change

❌ 使用useEffect在筛选条件变更时清空选择

typescript
// WRONG - ESLint error, causes cascading renders
useEffect(() => {
  setSelectedIds(new Set());
}, [filter]);
typescript
// 错误用法 - 会触发ESLint错误,导致级联渲染
useEffect(() => {
  setSelectedIds(new Set());
}, [filter]);

✅ Clear selection in the filter handler

✅ 在筛选处理函数中直接清空选择

typescript
// CORRECT - Clear inline when filter changes
const handleFilterChange = (value: string) => {
  setFilter(value);
  setSelectedIds(new Set());
};
typescript
// 正确用法 - 筛选条件变更时直接清空
const handleFilterChange = (value: string) => {
  setFilter(value);
  setSelectedIds(new Set());
};

❌ Selection state in table component

❌ 选择状态存放在表格组件中

typescript
// WRONG - State should be lifted to page
function Table() {
  const [selectedIds, setSelectedIds] = useState(new Set());
  // ...
}
typescript
// 错误用法 - 状态应提升至页面组件
function Table() {
  const [selectedIds, setSelectedIds] = useState(new Set());
  // ...
}

✅ Selection state in page component

✅ 选择状态存放在页面组件中

typescript
// CORRECT - Page owns state, passes to children
function Page() {
  const [selectedIds, setSelectedIds] = useState(new Set());
  return (
    <>
      <Table selectedIds={selectedIds} onSelectionChange={setSelectedIds} />
      <Toolbar selectedIds={selectedIds} onClearSelection={() => setSelectedIds(new Set())} />
    </>
  );
}
typescript
// 正确用法 - 页面组件拥有状态,传递给子组件
function Page() {
  const [selectedIds, setSelectedIds] = useState(new Set());
  return (
    <>
      <Table selectedIds={selectedIds} onSelectionChange={setSelectedIds} />
      <Toolbar selectedIds={selectedIds} onClearSelection={() => setSelectedIds(new Set())} />
    </>
  );
}

❌ Missing stopPropagation on row checkbox

❌ 行内复选框未阻止事件冒泡

typescript
// WRONG - Clicking checkbox also triggers row click
<TableRow onClick={onRowClick}>
  <TableCell>
    <Checkbox onCheckedChange={handleSelect} />
  </TableCell>
</TableRow>
typescript
// 错误用法 - 点击复选框会同时触发行点击事件
<TableRow onClick={onRowClick}>
  <TableCell>
    <Checkbox onCheckedChange={handleSelect} />
  </TableCell>
</TableRow>

✅ Stop propagation on checkbox cell

✅ 在复选框单元格上阻止事件冒泡

typescript
// CORRECT - Checkbox click doesn't bubble to row
<TableRow onClick={onRowClick}>
  <TableCell onClick={(e) => e.stopPropagation()}>
    <Checkbox onCheckedChange={handleSelect} />
  </TableCell>
</TableRow>
typescript
// 正确用法 - 点击复选框不会冒泡到行
<TableRow onClick={onRowClick}>
  <TableCell onClick={(e) => e.stopPropagation()}>
    <Checkbox onCheckedChange={handleSelect} />
  </TableCell>
</TableRow>

❌ No confirmation for destructive bulk actions

❌ 破坏性批量操作未添加确认步骤

typescript
// WRONG - Dangerous actions need confirmation
<Button onClick={onBulkDelete}>Delete All</Button>
typescript
// 错误用法 - 危险操作需要确认
<Button onClick={onBulkDelete}>Delete All</Button>

✅ Always confirm destructive actions

✅ 始终为破坏性操作添加确认步骤

typescript
// CORRECT - AlertDialog for confirmation
<Button onClick={() => setDeleteDialogOpen(true)}>Delete</Button>
<AlertDialog open={deleteDialogOpen}>
  {/* Confirmation content */}
</AlertDialog>
typescript
// 正确用法 - 使用AlertDialog进行确认
<Button onClick={() => setDeleteDialogOpen(true)}>Delete</Button>
<AlertDialog open={deleteDialogOpen}>
  {/* 确认内容 */}
</AlertDialog>

File Structure

文件结构

src/
├── app/
│   └── (dashboard)/
│       └── dashboard/
│           └── items/
│               └── page.tsx           # Selection state owner
├── components/
│   └── items/
│       ├── items-table.tsx            # Table with checkboxes
│       ├── items-bulk-actions-toolbar.tsx  # Floating toolbar
│       └── index.ts                   # Exports
└── hooks/
    └── use-items.ts                   # Include bulk mutation hooks
src/
├── app/
│   └── (dashboard)/
│       └── dashboard/
│           └── items/
│               └── page.tsx           # 选择状态拥有者
├── components/
│   └── items/
│       ├── items-table.tsx            # 带复选框的表格
│       ├── items-bulk-actions-toolbar.tsx  # 浮动工具栏
│       └── index.ts                   # 导出文件
└── hooks/
    └── use-items.ts                   # 包含批量变更钩子

Bulk Mutation Hooks Pattern

批量变更钩子模式

typescript
// hooks/use-items.ts
export function useBulkDeleteItems(teamId?: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (ids: string[]) => {
      const response = await fetch(`/api/teams/${teamId}/items/bulk-delete`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ids }),
      });
      if (!response.ok) throw new Error("Failed to delete items");
      return response.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["items", teamId] });
    },
  });
}
typescript
// hooks/use-items.ts
export function useBulkDeleteItems(teamId?: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (ids: string[]) => {
      const response = await fetch(`/api/teams/${teamId}/items/bulk-delete`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ids }),
      });
      if (!response.ok) throw new Error("Failed to delete items");
      return response.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["items", teamId] });
    },
  });
}

Related Asset Files

相关资源文件

AssetDescription
assets/components/bulk-actions-toolbar.tsx
Generic toolbar template
assets/components/selectable-table.tsx
Table with checkbox selection
assets/hooks/use-bulk-selection.ts
Reusable selection hook
资源描述
assets/components/bulk-actions-toolbar.tsx
通用工具栏模板
assets/components/selectable-table.tsx
带复选框选择的表格
assets/hooks/use-bulk-selection.ts
可复用的选择钩子

Checklist

检查清单

  • Selection state lives in page component (not table)
  • Checkbox uses
    checked={allSelected ? true : someSelected ? "indeterminate" : false}
  • Floating toolbar has
    fixed bottom-6 left-1/2 -translate-x-1/2 z-50
  • Toolbar has
    animate-in fade-in slide-in-from-bottom-4 duration-200
  • Toolbar returns
    null
    when
    selectedCount === 0
  • Row checkbox cell has
    onClick={(e) => e.stopPropagation()}
  • Selected count section has
    border-r
    separator
  • Destructive action has confirmation dialog
  • Destructive button has
    text-red-600 hover:text-red-700 hover:bg-red-50
  • Confirmation action has
    bg-red-600 hover:bg-red-700
  • Selection clears when filters change (in handler, not useEffect)
  • Bulk mutation hooks invalidate queries on success
  • 选择状态存放在页面组件中(而非表格)
  • 复选框使用
    checked={allSelected ? true : someSelected ? "indeterminate" : false}
  • 浮动工具栏使用
    fixed bottom-6 left-1/2 -translate-x-1/2 z-50
  • 工具栏使用
    animate-in fade-in slide-in-from-bottom-4 duration-200
  • 未选择任何内容时工具栏返回
    null
  • 行内复选框单元格添加
    onClick={(e) => e.stopPropagation()}
  • 已选数量区域添加
    border-r
    分隔线
  • 破坏性操作带有确认对话框
  • 破坏性按钮使用
    text-red-600 hover:text-red-700 hover:bg-red-50
  • 确认操作按钮使用
    bg-red-600 hover:bg-red-700
  • 筛选条件变更时清空选择(在处理函数中,而非useEffect)
  • 批量变更钩子在成功时失效查询