feat(test-case+bug): 测试用例模块 + Bug 模块完整实现
需求验收体系: - 测试用例:五态状态机(待执行/执行中/通过/失败/阻塞) - Bug:六态状态机(待修复/修复中/已修复/验证中/已关闭/已拒绝) - Bug 通过测试用例间接关联需求(不冗余存版本) - Bug 默认修复人 = 关联需求的开发任务负责人 - 测试进度 = 已执行用例/总用例, 通过率 = 通过/已执行 UI: - 版本详情页新增"测试用例" Tab + "BUG" Tab - 测试用例:统计栏+筛选+按需求分组列表+详情抽屉+提BUG入口 - Bug:统计栏+筛选+列表+详情抽屉(链式跳转用例→需求) - 概览胶囊"测试"阶段进度联动 - "与我相关"新增测试用例/Bug分组 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<TabKey>('all');
|
||||
const [completingPlan, setCompletingPlan] = useState<VersionPlan | null>(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() {
|
||||
<h2 className="text-[14px] font-semibold text-[var(--ink)]">
|
||||
{TABS.find((t) => t.key === activeTab)?.label}
|
||||
</h2>
|
||||
<span className="ml-2 text-[12px] text-[var(--ink-muted)]">{activeTab === 'devTask' ? myDevTasks.length : filtered.length} 项</span>
|
||||
<span className="ml-2 text-[12px] text-[var(--ink-muted)]">{activeTab === 'devTask' ? myDevTasks.length : activeTab === 'testCase' ? myTestCases.length : activeTab === 'bug' ? myBugs.length : filtered.length} 项</span>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-5 bg-[var(--bg)] space-y-3">
|
||||
@@ -150,6 +174,49 @@ export default function WorkspacePage() {
|
||||
);
|
||||
})
|
||||
)
|
||||
) : activeTab === 'testCase' ? (
|
||||
myTestCases.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
|
||||
<p className="text-[13px] text-[var(--ink-muted)]">暂无测试用例</p>
|
||||
</div>
|
||||
) : (
|
||||
myTestCases.map((tc) => (
|
||||
<div key={tc.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${TEST_CASE_STATUS_COLOR[tc.status]}`}>
|
||||
{TEST_CASE_STATUS_LABEL[tc.status]}
|
||||
</span>
|
||||
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{tc.caseNo}</span>
|
||||
<span className="text-[14px] font-medium text-[var(--ink)]">{tc.title}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-[var(--ink-muted)]">{tc.priority}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
) : activeTab === 'bug' ? (
|
||||
myBugs.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
|
||||
<p className="text-[13px] text-[var(--ink-muted)]">暂无 Bug</p>
|
||||
</div>
|
||||
) : (
|
||||
myBugs.map((bug) => (
|
||||
<div key={bug.id} className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[10px] font-medium px-2 py-0.5 rounded-full ${BUG_STATUS_COLOR[bug.status]}`}>
|
||||
{BUG_STATUS_LABEL[bug.status]}
|
||||
</span>
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded ${BUG_SEVERITY_COLOR[bug.severity]}`}>{BUG_SEVERITY_LABEL[bug.severity]}</span>
|
||||
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{bug.bugNo}</span>
|
||||
<span className="text-[14px] font-medium text-[var(--ink)]">{bug.title}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-[var(--ink-muted)]">{bug.priority}</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] p-12 text-center">
|
||||
<p className="text-[13px] text-[var(--ink-muted)]">暂无待办事项</p>
|
||||
|
||||
Reference in New Issue
Block a user