From 7fe3a3854a77d5be10d2f53d0e702d6294a75eab Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 12 Jun 2026 13:11:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(test-case+bug):=20=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E6=A8=A1=E5=9D=97=20+=20Bug=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 需求验收体系: - 测试用例:五态状态机(待执行/执行中/通过/失败/阻塞) - Bug:六态状态机(待修复/修复中/已修复/验证中/已关闭/已拒绝) - Bug 通过测试用例间接关联需求(不冗余存版本) - Bug 默认修复人 = 关联需求的开发任务负责人 - 测试进度 = 已执行用例/总用例, 通过率 = 通过/已执行 UI: - 版本详情页新增"测试用例" Tab + "BUG" Tab - 测试用例:统计栏+筛选+按需求分组列表+详情抽屉+提BUG入口 - Bug:统计栏+筛选+列表+详情抽屉(链式跳转用例→需求) - 概览胶囊"测试"阶段进度联动 - "与我相关"新增测试用例/Bug分组 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/app/versions/[id]/page.tsx | 26 +++ apps/web/app/workspace/page.tsx | 77 +++++++- apps/web/components/bug/BugCreateModal.tsx | 117 ++++++++++++ apps/web/components/bug/BugDetailDrawer.tsx | 133 ++++++++++++++ apps/web/components/bug/BugRow.tsx | 32 ++++ apps/web/components/bug/BugStatusBadge.tsx | 12 ++ apps/web/components/bug/BugTab.tsx | 128 ++++++++++++++ .../test-case/TestCaseCreateModal.tsx | 93 ++++++++++ .../test-case/TestCaseDetailDrawer.tsx | 166 ++++++++++++++++++ apps/web/components/test-case/TestCaseRow.tsx | 33 ++++ .../test-case/TestCaseStatusBadge.tsx | 12 ++ apps/web/components/test-case/TestCaseTab.tsx | 141 +++++++++++++++ apps/web/lib/bug.ts | 75 ++++++++ apps/web/lib/test-case.ts | 69 ++++++++ apps/web/stores/useBugStore.ts | 92 ++++++++++ apps/web/stores/useTestCaseStore.ts | 95 ++++++++++ 16 files changed, 1296 insertions(+), 5 deletions(-) create mode 100644 apps/web/components/bug/BugCreateModal.tsx create mode 100644 apps/web/components/bug/BugDetailDrawer.tsx create mode 100644 apps/web/components/bug/BugRow.tsx create mode 100644 apps/web/components/bug/BugStatusBadge.tsx create mode 100644 apps/web/components/bug/BugTab.tsx create mode 100644 apps/web/components/test-case/TestCaseCreateModal.tsx create mode 100644 apps/web/components/test-case/TestCaseDetailDrawer.tsx create mode 100644 apps/web/components/test-case/TestCaseRow.tsx create mode 100644 apps/web/components/test-case/TestCaseStatusBadge.tsx create mode 100644 apps/web/components/test-case/TestCaseTab.tsx create mode 100644 apps/web/lib/bug.ts create mode 100644 apps/web/lib/test-case.ts create mode 100644 apps/web/stores/useBugStore.ts create mode 100644 apps/web/stores/useTestCaseStore.ts diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index 8993303..15eecdc 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -18,8 +18,12 @@ import { OVERTIME_REASON_LABEL } from '@/lib/overtime'; import { VersionRequirementsTab } from '@/components/version/VersionRequirementsTab'; import { PlanTab } from '@/components/version/PlanTab'; import { DevTaskTab } from '@/components/dev-task/DevTaskTab'; +import { TestCaseTab } from '@/components/test-case/TestCaseTab'; +import { BugTab } from '@/components/bug/BugTab'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; +import { useTestCaseStore } from '@/stores/useTestCaseStore'; +import { useBugStore } from '@/stores/useBugStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { calcGroupProgress as calcDevTaskProgress } from '@/lib/dev-task'; @@ -51,6 +55,8 @@ export default function VersionDetailPage() { const { records, fetchRecords } = useOvertimeStore(); const { plans, fetchPlans, createPlan, updatePlan, completePlan, deletePlan } = useVersionPlanStore(); const { tasks: devTasks, fetchTasks: fetchDevTasks } = useDevTaskStore(); + const { testCases, fetchTestCases } = useTestCaseStore(); + const { bugs, fetchBugs } = useBugStore(); const user = useAuthStore((s) => s.user); const [activeTab, setActiveTab] = useState('overview'); @@ -59,6 +65,8 @@ export default function VersionDetailPage() { useEffect(() => { fetchRecords(); }, [fetchRecords]); useEffect(() => { fetchPlans(); }, [fetchPlans]); useEffect(() => { fetchDevTasks(); }, [fetchDevTasks]); + useEffect(() => { fetchTestCases(); }, [fetchTestCases]); + useEffect(() => { fetchBugs(); }, [fetchBugs]); const version = useMemo(() => getVersionDetail(overview, versionId), [overview, versionId]); @@ -299,6 +307,14 @@ export default function VersionDetailPage() { stageProgress['dev'] = { percent: devProgress, daysSpent: devDays }; } + // 测试阶段进度由 TestCase 完成率驱动 + const versionTestCases = testCases.filter((c) => versionReqs.some((r) => r.id === c.requirementId)); + 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); + stageProgress['testing'] = { percent: testPercent, daysSpent: 0 }; + } + return ; })()} @@ -476,6 +492,16 @@ export default function VersionDetailPage() { /> ); })() + ) : activeTab === 'testcases' ? ( + (() => { + const versionReqs = requirements.filter((r) => r.versionId === version.id); + return r.id)} />; + })() + ) : activeTab === 'bugs' ? ( + (() => { + const versionReqs = requirements.filter((r) => r.versionId === version.id); + return r.id)} />; + })() ) : (
功能开发中,敬请期待 diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index cf370c2..4c0b7a3 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -2,20 +2,24 @@ import { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { Search, FileText, Palette, Layout, ClipboardList, Check, ExternalLink, Link2, FileUp, Code2 } from 'lucide-react'; +import { Search, FileText, Palette, Layout, ClipboardList, Check, ExternalLink, Link2, FileUp, Code2, ClipboardCheck, Bug as BugIcon } from 'lucide-react'; import { useProductStore } from '@/stores/useProductStore'; import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; +import { useTestCaseStore } from '@/stores/useTestCaseStore'; +import { useBugStore } from '@/stores/useBugStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { flattenVersions } from '@/lib/derive'; import { calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan'; import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, formatHours } from '@/lib/dev-task'; +import { TEST_CASE_STATUS_LABEL, TEST_CASE_STATUS_COLOR } from '@/lib/test-case'; +import { BUG_STATUS_LABEL, BUG_STATUS_COLOR, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; import type { PlanTask, VersionPlan } from '@/lib/version-plan'; import type { DevTask } from '@/lib/dev-task'; -type TabKey = 'all' | 'research' | 'product' | 'ui' | 'devTask'; +type TabKey = 'all' | 'research' | 'product' | 'ui' | 'devTask' | 'testCase' | 'bug'; const TABS: { key: TabKey; label: string; icon: any }[] = [ { key: 'all', label: '全部待办', icon: ClipboardList }, @@ -23,6 +27,8 @@ const TABS: { key: TabKey; label: string; icon: any }[] = [ { key: 'product', label: '产品方案', icon: FileText }, { key: 'ui', label: 'UI设计', icon: Palette }, { key: 'devTask', label: '开发任务', icon: Code2 }, + { key: 'testCase', label: '测试用例', icon: ClipboardCheck }, + { key: 'bug', label: 'Bug', icon: BugIcon }, ]; export default function WorkspacePage() { @@ -32,6 +38,8 @@ export default function WorkspacePage() { const { requirements, fetchRequirements } = useRequirementStore(); const { tasks: devTasks, fetchTasks } = useDevTaskStore(); const { categories, fetchCategories } = useTaskCategoryStore(); + const { testCases, fetchTestCases } = useTestCaseStore(); + const { bugs, fetchBugs } = useBugStore(); const user = useAuthStore((s) => s.user); const [activeTab, setActiveTab] = useState('all'); const [completingPlan, setCompletingPlan] = useState(null); @@ -41,6 +49,8 @@ export default function WorkspacePage() { useEffect(() => { fetchRequirements(); }, [fetchRequirements]); useEffect(() => { fetchTasks(); }, [fetchTasks]); useEffect(() => { fetchCategories(); }, [fetchCategories]); + useEffect(() => { fetchTestCases(); }, [fetchTestCases]); + useEffect(() => { fetchBugs(); }, [fetchBugs]); const userName = user?.name ?? ''; const allVersions = useMemo(() => flattenVersions(overview), [overview]); @@ -57,15 +67,29 @@ export default function WorkspacePage() { [devTasks, userName] ); + // 我负责的待执行/执行中/失败测试用例 + const myTestCases = useMemo(() => + testCases.filter((c) => c.assigneeId === userName && (c.status === 'pending' || c.status === 'running' || c.status === 'failed')), + [testCases, userName] + ); + + // 我负责的未关闭 Bug + const myBugs = useMemo(() => + bugs.filter((b) => b.assigneeId === userName && b.status !== 'closed' && b.status !== 'rejected'), + [bugs, userName] + ); + const counts = { - all: myPlans.length + myDevTasks.length, + all: myPlans.length + myDevTasks.length + myTestCases.length + myBugs.length, research: myPlans.filter((p) => p.type === 'research').length, product: myPlans.filter((p) => p.type === 'product').length, ui: myPlans.filter((p) => p.type === 'ui').length, devTask: myDevTasks.length, + testCase: myTestCases.length, + bug: myBugs.length, }; - const filtered = activeTab === 'all' ? myPlans : activeTab === 'devTask' ? [] : myPlans.filter((p) => p.type === activeTab); + const filtered = activeTab === 'all' ? myPlans : (activeTab === 'devTask' || activeTab === 'testCase' || activeTab === 'bug') ? [] : myPlans.filter((p) => p.type === activeTab); const today = new Date().toISOString().slice(0, 10); const toggleTask = (plan: VersionPlan, task: PlanTask) => { @@ -113,7 +137,7 @@ export default function WorkspacePage() {

{TABS.find((t) => t.key === activeTab)?.label}

- {activeTab === 'devTask' ? myDevTasks.length : filtered.length} 项 + {activeTab === 'devTask' ? myDevTasks.length : activeTab === 'testCase' ? myTestCases.length : activeTab === 'bug' ? myBugs.length : filtered.length} 项
@@ -150,6 +174,49 @@ export default function WorkspacePage() { ); }) ) + ) : activeTab === 'testCase' ? ( + myTestCases.length === 0 ? ( +
+

暂无测试用例

+
+ ) : ( + myTestCases.map((tc) => ( +
+
+
+ + {TEST_CASE_STATUS_LABEL[tc.status]} + + {tc.caseNo} + {tc.title} +
+ {tc.priority} +
+
+ )) + ) + ) : activeTab === 'bug' ? ( + myBugs.length === 0 ? ( +
+

暂无 Bug

+
+ ) : ( + myBugs.map((bug) => ( +
+
+
+ + {BUG_STATUS_LABEL[bug.status]} + + {BUG_SEVERITY_LABEL[bug.severity]} + {bug.bugNo} + {bug.title} +
+ {bug.priority} +
+
+ )) + ) ) : filtered.length === 0 ? (

暂无待办事项

diff --git a/apps/web/components/bug/BugCreateModal.tsx b/apps/web/components/bug/BugCreateModal.tsx new file mode 100644 index 0000000..9ffdc86 --- /dev/null +++ b/apps/web/components/bug/BugCreateModal.tsx @@ -0,0 +1,117 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import { X } from 'lucide-react'; +import { useBugStore } from '@/stores/useBugStore'; +import { useTestCaseStore } from '@/stores/useTestCaseStore'; +import { useDevTaskStore } from '@/stores/useDevTaskStore'; +import { useRequirementStore } from '@/stores/useRequirementStore'; +import { useMemberStore } from '@/stores/useMemberStore'; +import { useAuthStore } from '@/stores/useAuthStore'; +import type { Priority } from '@/lib/derive'; +import type { BugSeverity } from '@/lib/bug'; + +interface Props { + testCaseId: string; + onClose: () => void; +} + +export function BugCreateModal({ testCaseId, onClose }: Props) { + const { createBug } = useBugStore(); + const { testCases } = useTestCaseStore(); + const { tasks: devTasks } = useDevTaskStore(); + const { requirements } = useRequirementStore(); + const { members } = useMemberStore(); + const user = useAuthStore((s) => s.user); + + const tc = testCases.find((c) => c.id === testCaseId); + const requirement = requirements.find((r) => r.id === tc?.requirementId); + + // 推导默认负责人 + const defaultAssignee = useMemo(() => { + if (!tc) return ''; + const reqTasks = devTasks.filter((t) => t.requirementId === tc.requirementId); + const inProgress = reqTasks.filter((t) => t.status === 'in_progress').sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + if (inProgress.length > 0) return inProgress[0].assigneeId; + const latest = reqTasks.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + return latest.length > 0 ? latest[0].assigneeId : ''; + }, [tc, devTasks]); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [severity, setSeverity] = useState('major'); + const [priority, setPriority] = useState(tc?.priority || 'P1'); + const [assigneeId, setAssigneeId] = useState(defaultAssignee); + + const canSubmit = title.trim() && description.trim() && assigneeId; + + const handleSubmit = () => { + if (!canSubmit) return; + createBug({ + testCaseId, + title: title.trim(), + description: description.trim(), + severity, + priority, + reportedBy: user?.name || '系统', + assigneeId, + }); + onClose(); + }; + + return ( +
+
e.stopPropagation()}> +
+

提交 Bug

+ +
+ + {/* 只读关联信息 */} +
+
关联用例:{tc?.caseNo} {tc?.title}
+ {requirement &&
关联需求:{requirement.code} {requirement.title}
} +
+ +
+
+ + 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="简要描述问题" /> +
+
+ +