'use client'; import { useState, useMemo, useEffect } from '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'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { TestCaseRow } from './TestCaseRow'; import { TestCaseCreateModal } from './TestCaseCreateModal'; import { TestCaseDetailDrawer } from './TestCaseDetailDrawer'; import { BugCreateModal } from '@/components/bug/BugCreateModal'; import { calcTestProgress, TEST_CASE_STATUS_LABEL } from '@/lib/test-case'; import { Pagination, usePagination } from '@/components/Pagination'; import type { TestCaseStatus } from '@/lib/test-case'; interface Props { versionId: string; requirementIds: string[]; } export function TestCaseTab({ versionId, requirementIds }: Props) { const { testCases, fetchTestCases, deleteTestCase } = useTestCaseStore(); const { bugs, fetchBugs } = useBugStore(); const { tasks: devTasks } = useDevTaskStore(); const { requirements } = useRequirementStore(); const user = useAuthStore((s) => s.user); useEffect(() => { fetchTestCases(); }, [fetchTestCases]); useEffect(() => { fetchBugs(); }, [fetchBugs]); const versionCases = useMemo( () => testCases.filter((c) => c.versionId === versionId), [testCases, versionId], ); // 提测状态判断 const versionDevTasks = useMemo(() => devTasks.filter((t) => requirementIds.includes(t.requirementId)), [devTasks, requirementIds]); const allSubmitted = versionDevTasks.length > 0 && versionDevTasks.every((t) => t.status === 'submitted'); const stats = calcTestProgress(versionCases); // 筛选 const [filterAssignee, setFilterAssignee] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const filteredCases = useMemo(() => { let result = versionCases; if (filterAssignee) result = result.filter((c) => c.assigneeId === filterAssignee); if (filterStatus) result = result.filter((c) => c.status === filterStatus); return result; }, [versionCases, filterAssignee, filterStatus]); const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filteredCases, 20); const [showCreate, setShowCreate] = useState(false); 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(); for (const c of paged) { const key = c.requirementId || '__none__'; const list = map.get(key) || []; list.push(c); map.set(key, list); } return map; }, [paged]); const assignees = useMemo(() => { const names = new Set(versionCases.map((c) => c.assigneeId).filter(Boolean) as string[]); return Array.from(names); }, [versionCases]); const hasFilter = !!(filterAssignee || filterStatus); return (
{/* 提测通知 */} {allSubmitted && (
开发已全部提测,可以开始测试 {versionDevTasks.length > 0 && {versionDevTasks.length} 个开发任务已交付}
)} {versionDevTasks.length > 0 && !allSubmitted && (
开发进行中,尚未全部提测({versionDevTasks.filter((t) => t.status === 'submitted').length}/{versionDevTasks.length})
)} {/* 统计栏 */}
测试完成 {stats.completionRate}%
{stats.total} 用例 · 通过 {stats.passed} · 失败 {stats.failed} · 阻塞 {stats.blocked} 通过率 {stats.passRate}%
{/* 筛选 */}
{hasFilter && } {selectedIds.size > 0 && ( )} {filteredCases.length} 条
{/* 列表 */} {filteredCases.length === 0 ? (

{hasFilter ? '没有匹配的用例' : '暂无测试用例'}

{!hasFilter && }
) : ( <>
全选当页
{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) => (
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 && } {showCreate && setShowCreate(false)} />} {selectedCaseId && setSelectedCaseId(null)} onCreateBug={(id) => { setBugForCaseId(id); }} />} {bugForCaseId && setBugForCaseId(null)} />}
); }