diff --git a/apps/web/components/dev-task/DevTaskTab.tsx b/apps/web/components/dev-task/DevTaskTab.tsx index e5a0a5c..b76eeee 100644 --- a/apps/web/components/dev-task/DevTaskTab.tsx +++ b/apps/web/components/dev-task/DevTaskTab.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useMemo, useEffect } from 'react'; -import { Plus, Code2, Filter, Trash2 } from 'lucide-react'; +import { Plus, Code2, Trash2 } from 'lucide-react'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; import { useTaskWorklogStore } from '@/stores/useTaskWorklogStore'; @@ -38,11 +38,11 @@ export function DevTaskTab({ versionId, requirementIds, versionDeadline }: Props [tasks, requirementIds], ); - // 筛选状态 - const [filterAssignee, setFilterAssignee] = useState(''); - const [filterStatus, setFilterStatus] = useState(''); - const [filterBlocked, setFilterBlocked] = useState(''); - const [filterCategory, setFilterCategory] = useState(''); + // 筛选 + const [filterAssignee, setFilterAssignee] = useState(''); + const [filterStatus, setFilterStatus] = useState(''); + const [filterBlocked, setFilterBlocked] = useState(''); + const [filterCategory, setFilterCategory] = useState(''); const filteredTasks = useMemo(() => { let result = versionTasks; @@ -56,10 +56,8 @@ export function DevTaskTab({ versionId, requirementIds, versionDeadline }: Props const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filteredTasks, 20); - // 统计 const progress = calcGroupProgress(versionTasks); const totalEstimate = versionTasks.reduce((s, t) => s + t.estimateHours, 0); - const totalActual = versionTasks.reduce((s, t) => s + t.actualHours, 0); const blockedCount = versionTasks.filter((t) => t.isBlocked).length; const [showCreate, setShowCreate] = useState(false); @@ -74,15 +72,9 @@ export function DevTaskTab({ versionId, requirementIds, versionDeadline }: Props 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((t) => t.id))); - } + setSelectedIds(isAllSelected ? new Set() : new Set(paged.map((t) => t.id))); }; - const handleBatchDelete = () => { if (selectedIds.size === 0) return; if (!confirm(`确定删除选中的 ${selectedIds.size} 个任务?`)) return; @@ -90,7 +82,6 @@ export function DevTaskTab({ versionId, requirementIds, versionDeadline }: Props setSelectedIds(new Set()); }; - // 按需求分组 const groupedByReq = useMemo(() => { const map = new Map(); for (const t of paged) { @@ -101,112 +92,93 @@ 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); - }, [versionTasks]); - + const assignees = useMemo(() => Array.from(new Set(versionTasks.map((t) => t.assigneeId))), [versionTasks]); const hasFilter = !!(filterAssignee || filterStatus || filterBlocked || filterCategory); return (
- {/* 统计栏 */} -
-
- - 开发进度 {progress}% + {/* 工具栏:进度 + 筛选 + 操作 合为一行 */} +
+ {/* 进度 */} + + {progress}% +
+
-
-
-
- - {versionTasks.length} 任务 · 投入 {totalActual}h / 预计 {totalEstimate}h - - {blockedCount > 0 && ( - {blockedCount} 阻塞 - )} - -
+ {versionTasks.length}任务 · {totalEstimate}h + {blockedCount > 0 && {blockedCount}阻塞} - {/* 筛选栏 + 批量操作 */} -
- - { setFilterAssignee(e.target.value); setPage(1); }} className="h-6 rounded border border-[var(--line)] bg-[var(--bg-card)] px-1.5 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none"> + + {user?.name && } {assignees.filter((a) => a !== user?.name).map((a) => )} - { setFilterStatus(e.target.value); setPage(1); }} className="h-6 rounded border border-[var(--line)] bg-[var(--bg-card)] px-1.5 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none"> + {(Object.entries(DEV_TASK_STATUS_LABEL) as [DevTaskStatus, string][]).map(([k, v]) => )} - { setFilterBlocked(e.target.value); setPage(1); }} className="h-6 rounded border border-[var(--line)] bg-[var(--bg-card)] px-1.5 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none"> + - { setFilterCategory(e.target.value); setPage(1); }} className="h-6 rounded border border-[var(--line)] bg-[var(--bg-card)] px-1.5 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none"> + {categories.map((c) => )} - {hasFilter && ( - - )} - {selectedIds.size > 0 && ( - } + + {/* 右侧操作 */} +
+ {selectedIds.size > 0 && ( + + )} + - )} - {filteredTasks.length} 条结果 +
- {/* 任务列表 */} + {/* 列表 */} {filteredTasks.length === 0 ? (

{hasFilter ? '没有匹配的任务' : '暂无开发任务'}

{!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}% -
- {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)} /> -
-
- ))} + Array.from(groupedByReq.entries()).map(([reqId, reqTasks]) => { + const req = requirements.find((r) => r.id === reqId); + const reqProgress = calcGroupProgress(reqTasks); + return ( +
+
+ selectedIds.has(t.id))} onChange={() => { const ids = reqTasks.map((t) => t.id); const allSelected = ids.every((id) => selectedIds.has(id)); const next = new Set(selectedIds); if (allSelected) ids.forEach((id) => next.delete(id)); else ids.forEach((id) => next.add(id)); setSelectedIds(next); }} className="h-3.5 w-3.5 rounded border-[var(--line)]" /> + {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)} /> +
+
+ ))} +
+ ); + }) )} - {/* 分页 */} - {total > 20 && ( - - )} + {total > 20 && } - {/* 弹窗 & 抽屉 */} {showCreate && setShowCreate(false)} />} {selectedTaskId && t.id)} onClose={() => setSelectedTaskId(null)} />}
diff --git a/apps/web/components/test-case/TestCaseTab.tsx b/apps/web/components/test-case/TestCaseTab.tsx index 193f5dd..6bb677e 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, Trash2 } from 'lucide-react'; +import { Plus, ClipboardCheck, CheckCircle2, Trash2 } from 'lucide-react'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useBugStore } from '@/stores/useBugStore'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; @@ -35,7 +35,7 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { [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'); @@ -67,12 +67,9 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { 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))); + setSelectedIds(isAllSelected ? new Set() : 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)); @@ -82,7 +79,7 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { setSelectedIds(new Set()); }; - // 按需求分组(无需求的归为"通用") + // 按需求分组 const groupedByReq = useMemo(() => { const map = new Map(); for (const c of paged) { @@ -94,65 +91,58 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { 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 assignees = useMemo(() => Array.from(new Set(versionCases.map((c) => c.assigneeId).filter(Boolean) as string[])), [versionCases]); const hasFilter = !!(filterAssignee || filterStatus); return (
{/* 提测通知 */} {allSubmitted && ( -
- +
+ 开发已全部提测,可以开始测试 - {versionDevTasks.length > 0 && {versionDevTasks.length} 个开发任务已交付} + {versionDevTasks.length} 个任务已交付
)} {versionDevTasks.length > 0 && !allSubmitted && ( -
- 开发进行中,尚未全部提测({versionDevTasks.filter((t) => t.status === 'submitted').length}/{versionDevTasks.length}) +
+ 开发进行中({versionDevTasks.filter((t) => t.status === 'submitted').length}/{versionDevTasks.length} 已提测)
)} - {/* 统计栏 */} -
-
- - 测试完成 {stats.completionRate}% -
-
-
-
- - {stats.total} 用例 · 通过 {stats.passed} · 失败 {stats.failed} · 阻塞 {stats.blocked} - - 通过率 {stats.passRate}% - -
- {/* 筛选 */} -
- - { setFilterAssignee(e.target.value); setPage(1); }} className="h-6 rounded border border-[var(--line)] bg-[var(--bg-card)] px-1.5 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none"> + + {user?.name && } {assignees.filter((a) => a !== user?.name).map((a) => )} - { setFilterStatus(e.target.value); setPage(1); }} className="h-6 rounded border border-[var(--line)] bg-[var(--bg-card)] px-1.5 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none"> + {(Object.entries(TEST_CASE_STATUS_LABEL) as [TestCaseStatus, string][]).map(([k, v]) => )} - {hasFilter && } - {selectedIds.size > 0 && ( - } + +
+ {selectedIds.size > 0 && ( + + )} + - )} - {filteredCases.length} 条 +
{/* 列表 */} @@ -162,19 +152,15 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { {!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 || '未关联需求'} + 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 ( +
+
+ selectedIds.has(c.id))} onChange={() => { const ids = cases.map((c) => c.id); const allSel = ids.every((id) => selectedIds.has(id)); const next = new Set(selectedIds); if (allSel) ids.forEach((id) => next.delete(id)); else ids.forEach((id) => next.add(id)); setSelectedIds(next); }} className="h-3.5 w-3.5 rounded border-[var(--line)]" /> + {req?.code || '通用'} + {req?.title || '未关联需求'} {reqPassed}/{cases.length} 通过
{cases.map((c) => ( @@ -189,14 +175,13 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { ))}
); - })} - + }) )} {total > 20 && } {showCreate && setShowCreate(false)} />} - {selectedCaseId && setSelectedCaseId(null)} onCreateBug={(id) => { setBugForCaseId(id); }} />} + {selectedCaseId && setSelectedCaseId(null)} onCreateBug={(id) => setBugForCaseId(id)} />} {bugForCaseId && setBugForCaseId(null)} />}
);