'use client';
import { useState } from 'react';
import { X, AlertTriangle, Link2, ChevronRight, Bug as BugIcon, Trash2 } from 'lucide-react';
import { TestCaseStatusBadge } from './TestCaseStatusBadge';
import { BugStatusBadge } from '@/components/bug/BugStatusBadge';
import { useTestCaseStore } from '@/stores/useTestCaseStore';
import { useBugStore } from '@/stores/useBugStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { TC_ALLOWED_TRANSITIONS, TEST_CASE_STATUS_LABEL } from '@/lib/test-case';
import { calcActualHoursByDates } from '@/lib/dev-task';
import { BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug';
import type { TestCaseStatus } from '@/lib/test-case';
interface Props {
testCaseId: string;
onClose: () => void;
onCreateBug?: (testCaseId: string) => void;
}
export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug }: Props) {
const { testCases, changeStatus, deleteTestCase } = useTestCaseStore();
const { bugs } = useBugStore();
const { requirements } = useRequirementStore();
const tc = testCases.find((c) => c.id === testCaseId);
if (!tc) return null;
const requirement = requirements.find((r) => r.id === tc.requirementId);
const relatedBugs = bugs.filter((b) => b.testCaseId === tc.id);
const nextStatuses = TC_ALLOWED_TRANSITIONS[tc.status];
const [failReason, setFailReason] = useState('');
const [blockReason, setBlockReason] = useState('');
const [showFailInput, setShowFailInput] = useState(false);
const [showBlockInput, setShowBlockInput] = useState(false);
const handleTransition = (to: TestCaseStatus) => {
if (to === 'failed') { setShowFailInput(true); return; }
if (to === 'blocked') { setShowBlockInput(true); return; }
changeStatus(tc.id, to);
};
const confirmFail = () => {
changeStatus(tc.id, 'failed', { failReason: failReason.trim() || undefined });
setShowFailInput(false);
setFailReason('');
};
const confirmBlock = () => {
changeStatus(tc.id, 'blocked', { blockReason: blockReason.trim() || undefined });
setShowBlockInput(false);
setBlockReason('');
};
return (
e.stopPropagation()}>
{tc.caseNo}
{tc.title}
{/* 关联需求 */}
{requirement && (
关联需求
{requirement.code}
{requirement.title}
)}
{/* 状态 & 操作 */}
状态 & 操作
{tc.executedAt && 执行于 {tc.executedAt}}
{nextStatuses.length > 0 && !showFailInput && !showBlockInput && (
{nextStatuses.map((s) => (
))}
)}
{showFailInput && (
setFailReason(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') confirmFail(); }} placeholder="失败原因(可选)" className="flex-1 h-8 rounded-lg border border-[var(--line)] px-3 text-[12px] focus:border-red-400 focus:outline-none" autoFocus />
)}
{showBlockInput && (
setBlockReason(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') confirmBlock(); }} placeholder="阻塞原因(可选)" className="flex-1 h-8 rounded-lg border border-[var(--line)] px-3 text-[12px] focus:border-orange-400 focus:outline-none" autoFocus />
)}
{tc.failReason &&
}
{tc.blockReason &&
}
{/* 基本信息 */}
基本信息
优先级:{tc.priority}
负责人:{tc.assigneeId || '-'}
创建人:{tc.createdBy}
创建日期:{tc.createdAt.slice(0, 10)}
{tc.startedAt &&
开始执行:{tc.startedAt.slice(0, 16).replace('T', ' ')}
}
{tc.completedAt &&
执行完成:{tc.completedAt.slice(0, 16).replace('T', ' ')}
}
{tc.startedAt &&
测试耗时:{calcActualHoursByDates(tc.startedAt, tc.completedAt)}h
}
{/* 用例描述 */}
{tc.description && (
测试步骤 & 预期
{tc.description}
)}
{/* 关联 Bug + 提BUG按钮 */}
关联 Bug ({relatedBugs.length})
{tc.status === 'failed' && onCreateBug && (
)}
{relatedBugs.length === 0 ? (
暂无关联 Bug
) : (
{relatedBugs.map((bug) => (
{bug.bugNo}
{bug.title}
{BUG_SEVERITY_LABEL[bug.severity]}
))}
)}
);
}