需求验收体系: - 测试用例:五态状态机(待执行/执行中/通过/失败/阻塞) - Bug:六态状态机(待修复/修复中/已修复/验证中/已关闭/已拒绝) - Bug 通过测试用例间接关联需求(不冗余存版本) - Bug 默认修复人 = 关联需求的开发任务负责人 - 测试进度 = 已执行用例/总用例, 通过率 = 通过/已执行 UI: - 版本详情页新增"测试用例" Tab + "BUG" Tab - 测试用例:统计栏+筛选+按需求分组列表+详情抽屉+提BUG入口 - Bug:统计栏+筛选+列表+详情抽屉(链式跳转用例→需求) - 概览胶囊"测试"阶段进度联动 - "与我相关"新增测试用例/Bug分组 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
7.3 KiB
TypeScript
134 lines
7.3 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { X, Link2, ChevronRight } from 'lucide-react';
|
||
import { BugStatusBadge } from './BugStatusBadge';
|
||
import { useBugStore } from '@/stores/useBugStore';
|
||
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||
import { useRequirementStore } from '@/stores/useRequirementStore';
|
||
import { BUG_ALLOWED_TRANSITIONS, BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug';
|
||
import type { BugStatus } from '@/lib/bug';
|
||
|
||
interface Props {
|
||
bugId: string;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export function BugDetailDrawer({ bugId, onClose }: Props) {
|
||
const { bugs, changeStatus } = useBugStore();
|
||
const { testCases } = useTestCaseStore();
|
||
const { requirements } = useRequirementStore();
|
||
|
||
const bug = bugs.find((b) => b.id === bugId);
|
||
if (!bug) return null;
|
||
|
||
const tc = testCases.find((c) => c.id === bug.testCaseId);
|
||
const requirement = tc ? requirements.find((r) => r.id === tc.requirementId) : null;
|
||
const nextStatuses = BUG_ALLOWED_TRANSITIONS[bug.status];
|
||
|
||
const [resolution, setResolution] = useState(bug.resolution || '');
|
||
const [showResolutionInput, setShowResolutionInput] = useState(false);
|
||
|
||
const handleTransition = (to: BugStatus) => {
|
||
if (to === 'fixed') { setShowResolutionInput(true); return; }
|
||
changeStatus(bug.id, to);
|
||
};
|
||
|
||
const confirmFix = () => {
|
||
changeStatus(bug.id, 'fixed', { resolution: resolution.trim() || undefined });
|
||
setShowResolutionInput(false);
|
||
};
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}>
|
||
<div className="w-full max-w-md h-full bg-[var(--bg)] border-l border-[var(--line)] shadow-[var(--shadow-lg)] flex flex-col" onClick={(e) => e.stopPropagation()}>
|
||
<div className="flex items-center justify-between h-14 px-5 border-b border-[var(--line)] bg-[var(--bg-card)] shrink-0">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-[12px] font-mono text-[var(--ink-muted)]">{bug.bugNo}</span>
|
||
<span className="text-[14px] font-semibold text-[var(--ink)] truncate max-w-[240px]">{bug.title}</span>
|
||
</div>
|
||
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-[var(--bg-subtle)]"><X className="h-4 w-4 text-[var(--ink-muted)]" /></button>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||
{/* 关联链路 */}
|
||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-3 space-y-2">
|
||
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide">关联链路</div>
|
||
{tc && (
|
||
<div className="flex items-center gap-2">
|
||
<Link2 className="h-3.5 w-3.5 text-[var(--accent)]" />
|
||
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{tc.caseNo}</span>
|
||
<span className="text-[13px] text-[var(--ink)]">{tc.title}</span>
|
||
</div>
|
||
)}
|
||
{requirement && (
|
||
<div className="flex items-center gap-2 pl-5">
|
||
<ChevronRight className="h-3 w-3 text-[var(--ink-muted)]" />
|
||
<span className="text-[11px] font-mono text-[var(--ink-muted)]">{requirement.code}</span>
|
||
<span className="text-[12px] text-[var(--ink-soft)]">{requirement.title}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 状态 & 操作 */}
|
||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4 space-y-3">
|
||
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide">状态 & 操作</div>
|
||
<div className="flex items-center gap-3">
|
||
<BugStatusBadge status={bug.status} />
|
||
<span className={`text-[10px] px-2 py-0.5 rounded ${BUG_SEVERITY_COLOR[bug.severity]}`}>{BUG_SEVERITY_LABEL[bug.severity]}</span>
|
||
<span className="text-[11px] text-[var(--ink-muted)]">{bug.priority}</span>
|
||
</div>
|
||
|
||
{nextStatuses.length > 0 && !showResolutionInput && (
|
||
<div className="flex items-center gap-2 pt-1 flex-wrap">
|
||
<ChevronRight className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
|
||
{nextStatuses.map((s) => (
|
||
<button key={s} onClick={() => handleTransition(s)} className={`h-8 px-4 rounded-lg text-[12px] font-medium transition-colors ${s === 'closed' ? 'bg-emerald-500 text-white hover:bg-emerald-600' : s === 'rejected' ? 'bg-zinc-400 text-white hover:bg-zinc-500' : 'bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]'}`}>
|
||
{BUG_STATUS_LABEL[s]}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{showResolutionInput && (
|
||
<div className="space-y-2 pt-1">
|
||
<input value={resolution} onChange={(e) => setResolution(e.target.value)} placeholder="修复说明" className="h-8 w-full rounded-lg border border-[var(--line)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" autoFocus />
|
||
<div className="flex gap-2">
|
||
<button onClick={confirmFix} className="h-8 px-3 rounded-lg text-[11px] font-medium bg-[var(--accent)] text-white">确认已修复</button>
|
||
<button onClick={() => setShowResolutionInput(false)} className="h-8 px-2 text-[11px] text-[var(--ink-muted)]">取消</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 基本信息 */}
|
||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-3">基本信息</div>
|
||
<div className="grid grid-cols-2 gap-y-3 gap-x-4 text-[12px]">
|
||
<div><span className="text-[var(--ink-muted)]">提交人:</span><span className="text-[var(--ink)]">{bug.reportedBy}</span></div>
|
||
<div><span className="text-[var(--ink-muted)]">修复人:</span><span className="text-[var(--ink)] font-medium">{bug.assigneeId}</span></div>
|
||
<div><span className="text-[var(--ink-muted)]">提交:</span><span className="text-[var(--ink)]">{bug.createdAt.slice(0, 10)}</span></div>
|
||
{bug.resolvedAt && <div><span className="text-[var(--ink-muted)]">修复:</span><span className="text-[var(--ink)]">{bug.resolvedAt}</span></div>}
|
||
{bug.closedAt && <div><span className="text-[var(--ink-muted)]">关闭:</span><span className="text-emerald-600">{bug.closedAt}</span></div>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 描述 */}
|
||
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-2">Bug 描述</div>
|
||
<p className="text-[12px] text-[var(--ink-soft)] leading-relaxed whitespace-pre-wrap">{bug.description}</p>
|
||
</div>
|
||
|
||
{/* 修复说明 */}
|
||
{bug.resolution && (
|
||
<div className="rounded-xl border border-emerald-200 bg-emerald-50 p-4">
|
||
<div className="text-[10px] text-emerald-700 uppercase tracking-wide mb-2">修复说明</div>
|
||
<p className="text-[12px] text-emerald-800 leading-relaxed whitespace-pre-wrap">{bug.resolution}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|