diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index 15eecdc..39c1c5f 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -8,7 +8,7 @@ import { useRequirementStore } from '@/stores/useRequirementStore'; import { useOvertimeStore } from '@/stores/useOvertimeStore'; import { getVersionDetail } from '@/lib/derive'; import { STAGES } from '@/lib/stage'; -import { VersionStatus, VERSION_STATUS_LABEL, VERSION_STATUS_BG, getVersionDisplayStatus } from '@/lib/version-status'; +import { VersionStatus, VERSION_STATUS_LABEL, VERSION_STATUS_BG, getVersionDisplayStatus, calcVersionExecutionStatus, EXECUTION_STATUS_LABEL, EXECUTION_STATUS_COLOR } from '@/lib/version-status'; import { CapsuleStages } from '@/components/version/CapsuleStages'; import { MemberChips } from '@/components/version/MemberChips'; import { HealthTrend, generateMockTrend } from '@/components/version/HealthTrend'; @@ -194,8 +194,17 @@ export default function VersionDetailPage() { const devTaskTodo = versionDevTasks.filter((t) => t.status === 'todo').length; const devTaskInProgress = versionDevTasks.filter((t) => t.status === 'in_progress').length; const devTaskBlocked = versionDevTasks.filter((t) => t.isBlocked).length; - const mockBugTotal = Math.max(Math.floor(versionReqs.length * 1.5), 2); - const mockBugOpen = Math.max(Math.floor(mockBugTotal * 0.3), 1); + const versionBugs = bugs.filter((b) => b.versionId === version.id); + const bugOpenCount = versionBugs.filter((b) => b.status === 'open' || b.status === 'fixing').length; + + // 版本执行态推导 + const versionTCs = testCases.filter((c) => c.versionId === version.id); + const executionStatus = calcVersionExecutionStatus({ + manualStatus: version.status, + devTasks: versionDevTasks, + testCases: versionTCs, + bugs: versionBugs, + }); return (
@@ -209,6 +218,9 @@ export default function VersionDetailPage() { {getVersionDisplayStatus(version.status, version.currentStage)} + + {EXECUTION_STATUS_LABEL[executionStatus]} + {healthScore} {HEALTH_LEVEL_LABEL[healthLevel]} @@ -230,7 +242,7 @@ export default function VersionDetailPage() { 0} /> - + 0} />
{/* 风险详情(胶囊条上方) */} @@ -308,7 +320,7 @@ export default function VersionDetailPage() { } // 测试阶段进度由 TestCase 完成率驱动 - const versionTestCases = testCases.filter((c) => versionReqs.some((r) => r.id === c.requirementId)); + const versionTestCases = testCases.filter((c) => c.versionId === version.id); if (versionTestCases.length > 0) { const executed = versionTestCases.filter((c) => c.status === 'passed' || c.status === 'failed' || c.status === 'blocked').length; const testPercent = Math.round((executed / versionTestCases.length) * 100); diff --git a/apps/web/components/bug/BugCreateModal.tsx b/apps/web/components/bug/BugCreateModal.tsx index 9ffdc86..a56f88d 100644 --- a/apps/web/components/bug/BugCreateModal.tsx +++ b/apps/web/components/bug/BugCreateModal.tsx @@ -48,7 +48,9 @@ export function BugCreateModal({ testCaseId, onClose }: Props) { const handleSubmit = () => { if (!canSubmit) return; createBug({ + versionId: tc?.versionId || '', testCaseId, + requirementId: tc?.requirementId || undefined, title: title.trim(), description: description.trim(), severity, diff --git a/apps/web/components/bug/BugTab.tsx b/apps/web/components/bug/BugTab.tsx index 175571d..3f3ab13 100644 --- a/apps/web/components/bug/BugTab.tsx +++ b/apps/web/components/bug/BugTab.tsx @@ -26,15 +26,10 @@ export function BugTab({ versionId, requirementIds }: Props) { useEffect(() => { fetchBugs(); }, [fetchBugs]); useEffect(() => { fetchTestCases(); }, [fetchTestCases]); - // 该版本的所有 Bug(通过测试用例 → 需求 关联) - const versionCases = useMemo( - () => testCases.filter((c) => requirementIds.includes(c.requirementId)), - [testCases, requirementIds], - ); - const versionCaseIds = useMemo(() => new Set(versionCases.map((c) => c.id)), [versionCases]); + // 该版本的所有 Bug(直接通过 versionId 查询) const versionBugs = useMemo( - () => bugs.filter((b) => versionCaseIds.has(b.testCaseId)), - [bugs, versionCaseIds], + () => bugs.filter((b) => b.versionId === versionId), + [bugs, versionId], ); // 统计 diff --git a/apps/web/components/test-case/TestCaseCreateModal.tsx b/apps/web/components/test-case/TestCaseCreateModal.tsx index 77a8f17..cb37616 100644 --- a/apps/web/components/test-case/TestCaseCreateModal.tsx +++ b/apps/web/components/test-case/TestCaseCreateModal.tsx @@ -9,11 +9,12 @@ import { useAuthStore } from '@/stores/useAuthStore'; import type { Priority } from '@/lib/derive'; interface Props { + versionId: string; requirementIds: string[]; onClose: () => void; } -export function TestCaseCreateModal({ requirementIds, onClose }: Props) { +export function TestCaseCreateModal({ versionId, requirementIds, onClose }: Props) { const { createTestCase } = useTestCaseStore(); const { requirements } = useRequirementStore(); const { members } = useMemberStore(); @@ -30,12 +31,13 @@ export function TestCaseCreateModal({ requirementIds, onClose }: Props) { const [assigneeId, setAssigneeId] = useState(user?.name || ''); const [description, setDescription] = useState(''); - const canSubmit = title.trim() && requirementId; + const canSubmit = title.trim(); const handleSubmit = () => { if (!canSubmit) return; createTestCase({ - requirementId, + versionId, + requirementId: requirementId || undefined, title: title.trim(), description: description.trim() || undefined, priority, @@ -58,8 +60,9 @@ export function TestCaseCreateModal({ requirementIds, onClose }: Props) { setTitle(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" placeholder="例如:正常排班" />
- +
diff --git a/apps/web/components/test-case/TestCaseTab.tsx b/apps/web/components/test-case/TestCaseTab.tsx index c2681a9..8d1dc44 100644 --- a/apps/web/components/test-case/TestCaseTab.tsx +++ b/apps/web/components/test-case/TestCaseTab.tsx @@ -29,7 +29,7 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { useEffect(() => { fetchBugs(); }, [fetchBugs]); const versionCases = useMemo( - () => testCases.filter((c) => requirementIds.includes(c.requirementId)), + () => testCases.filter((c) => c.versionId === versionId), [testCases, requirementIds], ); @@ -52,13 +52,14 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { const [selectedCaseId, setSelectedCaseId] = useState(null); const [bugForCaseId, setBugForCaseId] = useState(null); - // 按需求分组 + // 按需求分组(无需求的归为"通用") const groupedByReq = useMemo(() => { const map = new Map(); for (const c of paged) { - const list = map.get(c.requirementId) || []; + const key = c.requirementId || '__none__'; + const list = map.get(key) || []; list.push(c); - map.set(c.requirementId, list); + map.set(key, list); } return map; }, [paged]); @@ -133,7 +134,7 @@ export function TestCaseTab({ versionId, requirementIds }: Props) { {total > 20 && } - {showCreate && setShowCreate(false)} />} + {showCreate && setShowCreate(false)} />} {selectedCaseId && setSelectedCaseId(null)} onCreateBug={(id) => { setBugForCaseId(id); }} />} {bugForCaseId && setBugForCaseId(null)} />} diff --git a/apps/web/lib/bug.ts b/apps/web/lib/bug.ts index f40dfee..37372dc 100644 --- a/apps/web/lib/bug.ts +++ b/apps/web/lib/bug.ts @@ -6,7 +6,9 @@ export type BugSeverity = 'critical' | 'major' | 'minor' | 'trivial'; export interface Bug { id: string; bugNo: string; + versionId: string; testCaseId: string; + requirementId?: string; title: string; description: string; severity: BugSeverity; diff --git a/apps/web/lib/test-case.ts b/apps/web/lib/test-case.ts index cfbad0a..34aae9d 100644 --- a/apps/web/lib/test-case.ts +++ b/apps/web/lib/test-case.ts @@ -5,7 +5,8 @@ export type TestCaseStatus = 'pending' | 'running' | 'passed' | 'failed' | 'bloc export interface TestCase { id: string; caseNo: string; - requirementId: string; + versionId: string; + requirementId?: string; title: string; description?: string; priority: Priority; diff --git a/apps/web/lib/version-status.ts b/apps/web/lib/version-status.ts index f582d6a..3c83897 100644 --- a/apps/web/lib/version-status.ts +++ b/apps/web/lib/version-status.ts @@ -40,3 +40,66 @@ export function getVersionDisplayStatus(status: VersionStatus, currentStage?: st } return VERSION_STATUS_LABEL[status]; } + +// === 版本执行态推导 === + +export type VersionExecutionStatus = 'planning' | 'developing' | 'submitted' | 'testing' | 'ready' | 'released'; + +export const EXECUTION_STATUS_LABEL: Record = { + planning: '规划中', + developing: '开发中', + submitted: '已提测', + testing: '测试中', + ready: '可发布', + released: '已发布', +}; + +export const EXECUTION_STATUS_COLOR: Record = { + planning: 'bg-zinc-100 text-zinc-600', + developing: 'bg-blue-50 text-blue-600', + submitted: 'bg-orange-50 text-orange-600', + testing: 'bg-purple-50 text-purple-600', + ready: 'bg-emerald-50 text-emerald-600', + released: 'bg-zinc-100 text-zinc-500', +}; + +export interface ExecutionInput { + manualStatus?: VersionStatus; + devTasks: { status: string }[]; + testCases: { status: string }[]; + bugs: { status: string }[]; +} + +export function calcVersionExecutionStatus(input: ExecutionInput): VersionExecutionStatus { + const { manualStatus, devTasks, testCases, bugs } = input; + + if (manualStatus === 'released') return 'released'; + if (manualStatus === 'planned' || manualStatus === 'paused' || manualStatus === 'closed') return 'planning'; + + // fallback: 没有 DevTask 则停留 planning + if (devTasks.length === 0) return 'planning'; + + const allDevDone = devTasks.every((t) => t.status === 'done'); + const allDevSubmitted = devTasks.every((t) => t.status === 'submitted' || t.status === 'done'); + + // 判断是否可发布 + if (testCases.length > 0) { + const allPassed = testCases.every((c) => c.status === 'passed'); + const allBugsClosed = bugs.every((b) => b.status === 'closed' || b.status === 'rejected'); + if (allPassed && allBugsClosed) return 'ready'; + } + + // 测试中(有用例在执行) + if (allDevSubmitted && testCases.length > 0) { + const hasRunning = testCases.some((c) => c.status === 'running' || c.status === 'passed' || c.status === 'failed' || c.status === 'blocked'); + if (hasRunning) return 'testing'; + } + + // 已提测 + if (allDevSubmitted) return 'submitted'; + + // 开发完成但未全部提测 + if (allDevDone) return 'submitted'; + + return 'developing'; +} diff --git a/apps/web/stores/useBugStore.ts b/apps/web/stores/useBugStore.ts index 6bacf80..0c56b10 100644 --- a/apps/web/stores/useBugStore.ts +++ b/apps/web/stores/useBugStore.ts @@ -25,6 +25,7 @@ interface BugState { deleteBug: (id: string) => void; changeStatus: (id: string, to: BugStatus, extra?: { resolution?: string }) => { ok: boolean; message?: string }; getByTestCase: (caseId: string) => Bug[]; + getByVersion: (versionId: string) => Bug[]; getByAssignee: (assigneeId: string) => Bug[]; } @@ -86,6 +87,10 @@ export const useBugStore = create((set, get) => ({ return get().bugs.filter((b) => b.testCaseId === caseId); }, + getByVersion: (versionId) => { + return get().bugs.filter((b) => b.versionId === versionId); + }, + getByAssignee: (assigneeId) => { return get().bugs.filter((b) => b.assigneeId === assigneeId); }, diff --git a/apps/web/stores/useTestCaseStore.ts b/apps/web/stores/useTestCaseStore.ts index ad4f5c3..35fbf7c 100644 --- a/apps/web/stores/useTestCaseStore.ts +++ b/apps/web/stores/useTestCaseStore.ts @@ -25,6 +25,7 @@ interface TestCaseState { deleteTestCase: (id: string) => void; changeStatus: (id: string, to: TestCaseStatus, extra?: { failReason?: string; blockReason?: string }) => { ok: boolean; message?: string }; getByRequirement: (reqId: string) => TestCase[]; + getByVersion: (versionId: string) => TestCase[]; getByAssignee: (assigneeId: string) => TestCase[]; } @@ -89,6 +90,10 @@ export const useTestCaseStore = create((set, get) => ({ return get().testCases.filter((c) => c.requirementId === reqId); }, + getByVersion: (versionId) => { + return get().testCases.filter((c) => c.versionId === versionId); + }, + getByAssignee: (assigneeId) => { return get().testCases.filter((c) => c.assigneeId === assigneeId); },