();
for (const t of paged) {
@@ -78,7 +101,6 @@ export function DevTaskTab({ versionId, requirementIds, versionDeadline }: Props
return map;
}, [paged]);
- // 参与人列表
const assignees = useMemo(() => {
const names = new Set(versionTasks.map((t) => t.assigneeId));
return Array.from(names);
@@ -108,7 +130,7 @@ export function DevTaskTab({ versionId, requirementIds, versionDeadline }: Props
- {/* 筛选栏 */}
+ {/* 筛选栏 + 批量操作 */}
@@ -142,22 +169,36 @@ export function DevTaskTab({ versionId, requirementIds, versionDeadline }: Props
{!hasFilter && }
) : (
- Array.from(groupedByReq.entries()).map(([reqId, reqTasks]) => {
- const req = requirements.find((r) => r.id === reqId);
- const reqProgress = calcGroupProgress(reqTasks);
- return (
-
-
-
{req?.code}
-
{req?.title}
-
{reqProgress}%
+ <>
+ {/* 全选 */}
+
+
+ 全选当页
+
+ {Array.from(groupedByReq.entries()).map(([reqId, reqTasks]) => {
+ const req = requirements.find((r) => r.id === reqId);
+ const reqProgress = calcGroupProgress(reqTasks);
+ return (
+
+
+ {req?.code}
+ {req?.title}
+ {reqProgress}%
+
+ {reqTasks.map((t) => (
+
+
+ toggleSelect(t.id)} className="h-3.5 w-3.5 rounded border-[var(--line)]" onClick={(e) => e.stopPropagation()} />
+
+
+ c.id === t.categoryId)} onClick={() => setSelectedTaskId(t.id)} />
+
+
+ ))}
- {reqTasks.map((t) => (
-
c.id === t.categoryId)} onClick={() => setSelectedTaskId(t.id)} />
- ))}
-
- );
- })
+ );
+ })}
+ >
)}
{/* 分页 */}
diff --git a/apps/web/components/test-case/TestCaseDetailDrawer.tsx b/apps/web/components/test-case/TestCaseDetailDrawer.tsx
index 17b58fd..1220cdc 100644
--- a/apps/web/components/test-case/TestCaseDetailDrawer.tsx
+++ b/apps/web/components/test-case/TestCaseDetailDrawer.tsx
@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
-import { X, AlertTriangle, Link2, ChevronRight, Bug as BugIcon } from 'lucide-react';
+import { X, AlertTriangle, Link2, ChevronRight, Bug as BugIcon, Trash2 } from 'lucide-react';
import { TestCaseStatusBadge } from './TestCaseStatusBadge';
import { BugStatusBadge } from '@/components/bug/BugStatusBadge';
import { useTestCaseStore } from '@/stores/useTestCaseStore';
@@ -18,7 +18,7 @@ interface Props {
}
export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug }: Props) {
- const { testCases, changeStatus } = useTestCaseStore();
+ const { testCases, changeStatus, deleteTestCase } = useTestCaseStore();
const { bugs } = useBugStore();
const { requirements } = useRequirementStore();
@@ -60,7 +60,10 @@ export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug }: Props
{tc.caseNo}
{tc.title}
-
+
+
+
+
diff --git a/apps/web/components/test-case/TestCaseTab.tsx b/apps/web/components/test-case/TestCaseTab.tsx
index 86da422..193f5dd 100644
--- a/apps/web/components/test-case/TestCaseTab.tsx
+++ b/apps/web/components/test-case/TestCaseTab.tsx
@@ -1,7 +1,7 @@
'use client';
import { useState, useMemo, useEffect } from 'react';
-import { Plus, ClipboardCheck, Filter, CheckCircle2 } from 'lucide-react';
+import { Plus, ClipboardCheck, Filter, CheckCircle2, Trash2 } from 'lucide-react';
import { useTestCaseStore } from '@/stores/useTestCaseStore';
import { useBugStore } from '@/stores/useBugStore';
import { useDevTaskStore } from '@/stores/useDevTaskStore';
@@ -21,7 +21,7 @@ interface Props {
}
export function TestCaseTab({ versionId, requirementIds }: Props) {
- const { testCases, fetchTestCases } = useTestCaseStore();
+ const { testCases, fetchTestCases, deleteTestCase } = useTestCaseStore();
const { bugs, fetchBugs } = useBugStore();
const { tasks: devTasks } = useDevTaskStore();
const { requirements } = useRequirementStore();
@@ -58,6 +58,30 @@ export function TestCaseTab({ versionId, requirementIds }: Props) {
const [selectedCaseId, setSelectedCaseId] = useState(null);
const [bugForCaseId, setBugForCaseId] = useState(null);
+ // 批量选择
+ const [selectedIds, setSelectedIds] = useState>(new Set());
+ const isAllSelected = paged.length > 0 && paged.every((c) => selectedIds.has(c.id));
+
+ const toggleSelect = (id: string) => {
+ const next = new Set(selectedIds);
+ if (next.has(id)) next.delete(id); else next.add(id);
+ setSelectedIds(next);
+ };
+
+ const toggleAll = () => {
+ if (isAllSelected) setSelectedIds(new Set());
+ else setSelectedIds(new Set(paged.map((c) => c.id)));
+ };
+
+ const handleBatchDelete = () => {
+ if (selectedIds.size === 0) return;
+ const hasBug = Array.from(selectedIds).some((id) => bugs.some((b) => b.testCaseId === id));
+ if (hasBug) { alert('选中的用例中有关联 Bug 的用例,无法删除。请先取消关联 Bug 的用例选择。'); return; }
+ if (!confirm(`确定删除选中的 ${selectedIds.size} 个测试用例?`)) return;
+ selectedIds.forEach((id) => deleteTestCase(id));
+ setSelectedIds(new Set());
+ };
+
// 按需求分组(无需求的归为"通用")
const groupedByReq = useMemo(() => {
const map = new Map();
@@ -123,6 +147,11 @@ export function TestCaseTab({ versionId, requirementIds }: Props) {
{(Object.entries(TEST_CASE_STATUS_LABEL) as [TestCaseStatus, string][]).map(([k, v]) => )}
{hasFilter && }
+ {selectedIds.size > 0 && (
+
+ )}
{filteredCases.length} 条
@@ -133,22 +162,35 @@ export function TestCaseTab({ versionId, requirementIds }: Props) {
{!hasFilter && }
) : (
- Array.from(groupedByReq.entries()).map(([reqId, cases]) => {
- const req = requirements.find((r) => r.id === reqId);
- const reqPassed = cases.filter((c) => c.status === 'passed').length;
- return (
-
-
-
{req?.code}
-
{req?.title}
+ <>
+
+
+ 全选当页
+
+ {Array.from(groupedByReq.entries()).map(([reqId, cases]) => {
+ const req = reqId === '__none__' ? null : requirements.find((r) => r.id === reqId);
+ const reqPassed = cases.filter((c) => c.status === 'passed').length;
+ return (
+
+
+ {req?.code || '通用'}
+ {req?.title || '未关联需求'}
{reqPassed}/{cases.length} 通过
{cases.map((c) => (
-
b.testCaseId === c.id).length} onClick={() => setSelectedCaseId(c.id)} />
+
+
+ toggleSelect(c.id)} className="h-3.5 w-3.5 rounded border-[var(--line)]" onClick={(e) => e.stopPropagation()} />
+
+
+ b.testCaseId === c.id).length} onClick={() => setSelectedCaseId(c.id)} />
+
+
))}
);
- })
+ })}
+ >
)}
{total > 20 &&
}