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);
},