bulk-select-actions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhen 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
技术栈
| Package | Version | Purpose |
|---|---|---|
| ^1.x | Checkbox with indeterminate state |
| ^1.x | Entrance animations |
| latest | AlertDialog, Button, DropdownMenu |
| ^0.x | Icons |
| 包 | 版本 | 用途 |
|---|---|---|
| ^1.x | 支持不确定状态的复选框 |
| ^1.x | 入场动画 |
| latest | 提示对话框、按钮、下拉菜单 |
| ^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 prop with value:
checked"indeterminate"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 hookssrc/
├── 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
相关资源文件
| Asset | Description |
|---|---|
| Generic toolbar template |
| Table with checkbox selection |
| Reusable selection hook |
| 资源 | 描述 |
|---|---|
| 通用工具栏模板 |
| 带复选框选择的表格 |
| 可复用的选择钩子 |
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 when
nullselectedCount === 0 - Row checkbox cell has
onClick={(e) => e.stopPropagation()} - Selected count section has separator
border-r - 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)
- 批量变更钩子在成功时失效查询