feat(测试用例): 展示预估和实际耗时

This commit is contained in:
Script Generator
2026-06-25 14:55:45 +08:00
parent 88acf30296
commit 5723356d08
4 changed files with 108 additions and 18 deletions

View File

@@ -7,11 +7,12 @@ import { useBugStore } from '@/stores/useBugStore';
import { useDevTaskStore } from '@/stores/useDevTaskStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { useAuthStore } from '@/stores/useAuthStore';
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
import { TestCaseRow } from './TestCaseRow';
import { TestCaseCreateModal } from './TestCaseCreateModal';
import { TestCaseDetailDrawer } from './TestCaseDetailDrawer';
import { BugCreateModal } from '@/components/bug/BugCreateModal';
import { calcTestProgress, TEST_CASE_STATUS_LABEL, testCaseIntervals } from '@/lib/test-case';
import { aggregateTestCaseHours, calcTestProgress, TEST_CASE_STATUS_LABEL, testCaseIntervals } from '@/lib/test-case';
import { formatWorkHours, calcTwoMetrics } from '@/lib/work-hours';
import { Pagination, usePagination } from '@/components/Pagination';
import { SearchInput, matchTitleOrNo } from '@/components/SearchInput';
@@ -29,9 +30,11 @@ export function TestCaseTab({ versionId, requirementIds }: Props) {
const { tasks: devTasks } = useDevTaskStore();
const { requirements } = useRequirementStore();
const user = useAuthStore((s) => s.user);
const { categories, fetchCategories } = useTaskCategoryStore();
useEffect(() => { fetchTestCases(); }, [fetchTestCases]);
useEffect(() => { fetchBugs(); }, [fetchBugs]);
useEffect(() => { fetchCategories(); }, [fetchCategories]);
const reqIdSet = useMemo(() => new Set(requirementIds), [requirementIds]);
const versionCases = useMemo(
@@ -46,6 +49,7 @@ export function TestCaseTab({ versionId, requirementIds }: Props) {
const allSubmitted = versionDevTasks.length > 0 && versionDevTasks.every((t) => t.status === 'submitted');
const stats = calcTestProgress(versionCases);
const { estimate: tcEstimateHours, actual: tcActualHours } = aggregateTestCaseHours(versionCases);
const { calendarHours: tcCalendarHours, manhours: tcManhours } = useMemo(() => calcTwoMetrics(testCaseIntervals(versionCases)), [versionCases]);
const [filterAssignee, setFilterAssignee] = useState('');
@@ -95,6 +99,7 @@ export function TestCaseTab({ versionId, requirementIds }: Props) {
}, [paged]);
const requirementMap = useMemo(() => new Map(requirements.map((r) => [r.id, r])), [requirements]);
const categoryMap = useMemo(() => new Map(categories.map((c) => [c.id, c])), [categories]);
const bugCountByCase = useMemo(() => {
const map = new Map<string, number>();
for (const b of bugs) map.set(b.testCaseId, (map.get(b.testCaseId) ?? 0) + 1);
@@ -125,7 +130,7 @@ export function TestCaseTab({ versionId, requirementIds }: Props) {
<div className="h-1.5 w-20 rounded-full bg-[var(--bg)] overflow-hidden shrink-0">
<div className="h-full rounded-full bg-emerald-500" style={{ width: `${stats.completionRate}%` }} />
</div>
<span className="text-[11px] text-[var(--ink-muted)] shrink-0">{stats.total} · {stats.passed} · {stats.failed}</span>
<span className="text-[11px] text-[var(--ink-muted)] shrink-0">{stats.total} · {stats.passed} · {stats.failed} · {formatWorkHours(tcEstimateHours)} / {formatWorkHours(tcActualHours)}</span>
<span className="text-[11px] text-[var(--ink-muted)] shrink-0">{stats.passRate}%</span>
{tcManhours > 0 && (
<span className="text-[11px] text-[var(--ink-muted)] shrink-0"> {formatWorkHours(tcCalendarHours)} / {formatWorkHours(tcManhours)}</span>
@@ -180,7 +185,7 @@ export function TestCaseTab({ versionId, requirementIds }: Props) {
<input type="checkbox" checked={selectedIds.has(c.id)} onChange={() => toggleSelect(c.id)} className="h-3.5 w-3.5 rounded border-[var(--line)]" onClick={(e) => e.stopPropagation()} />
</div>
<div className="flex-1 min-w-0">
<TestCaseRow testCase={c} bugCount={bugCountByCase.get(c.id) ?? 0} onClick={() => setSelectedCaseId(c.id)} />
<TestCaseRow testCase={c} category={categoryMap.get(c.categoryId)} bugCount={bugCountByCase.get(c.id) ?? 0} onClick={() => setSelectedCaseId(c.id)} />
</div>
</div>
))}