Files
ftb-project-management/apps/web/components/bug/BugDetailDrawer.tsx
Script Generator c7bccd7819 fix: 测试用例提Bug按钮 + 全局侧边栏阴影统一
1. 测试用例"提 BUG"按钮接入工作台:
   - TestCaseDetailDrawer 传入 onCreateBug callback
   - 点击"提 BUG"关闭测试详情,弹出 BugCreateModal
   - 创建后 Bug 自动关联该测试用例

2. 全局侧边弹窗阴影统一为 shadow-2xl:
   - PlanDetailDrawer: shadow-2xl ✓
   - DevTaskDetailDrawer: shadow-[var(--shadow-lg)] → shadow-2xl
   - TestCaseDetailDrawer: shadow-[var(--shadow-lg)] → shadow-2xl
   - BugDetailDrawer: shadow-[var(--shadow-lg)] → shadow-2xl
   - 所有 Drawer 样式一致,阴影明显

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-15 17:45:09 +08:00

225 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState } from 'react';
import { X, Link2, ChevronRight, ArrowRightLeft } from 'lucide-react';
import { BugStatusBadge } from './BugStatusBadge';
import { useBugStore } from '@/stores/useBugStore';
import { useTestCaseStore } from '@/stores/useTestCaseStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { useMemberStore } from '@/stores/useMemberStore';
import { useAuthStore } from '@/stores/useAuthStore';
import { BUG_ALLOWED_TRANSITIONS, BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug';
import type { BugStatus } from '@/lib/bug';
const LOG_ACTION_LABEL: Record<string, string> = {
create: '创建',
status_change: '状态变更',
transfer: '转交',
resolve: '修复',
};
interface Props {
bugId: string;
onClose: () => void;
}
export function BugDetailDrawer({ bugId, onClose }: Props) {
const { bugs, changeStatus, transferBug } = useBugStore();
const { testCases } = useTestCaseStore();
const { requirements } = useRequirementStore();
const { members } = useMemberStore();
const user = useAuthStore((s) => s.user);
const operator = user?.name || '系统';
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 [showTransfer, setShowTransfer] = useState(false);
const [transferTo, setTransferTo] = useState('');
const [transferRemark, setTransferRemark] = useState('');
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null);
const handleTransition = (to: BugStatus) => {
if (to === 'fixed') { setShowResolutionInput(true); return; }
changeStatus(bug.id, to, operator);
};
const confirmFix = () => {
changeStatus(bug.id, 'fixed', operator, { resolution: resolution.trim() || undefined });
setShowResolutionInput(false);
};
const handleTransfer = () => {
if (!transferTo) return;
transferBug(bug.id, transferTo, operator, transferRemark.trim() || undefined);
setShowTransfer(false);
setTransferTo('');
setTransferRemark('');
};
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-2xl 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 && operator === bug.assigneeId && (
<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>
))}
{bug.status !== 'closed' && bug.status !== 'rejected' && (
<button onClick={() => setShowTransfer(!showTransfer)} className="h-8 px-3 rounded-lg text-[12px] font-medium border border-[var(--line)] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] flex items-center gap-1">
<ArrowRightLeft className="h-3 w-3" />
</button>
)}
</div>
)}
{nextStatuses.length > 0 && !showResolutionInput && operator !== bug.assigneeId && (
<div className="text-[11px] text-[var(--ink-muted)] pt-1"> {bug.assigneeId}</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>
)}
{showTransfer && (
<div className="space-y-2 pt-1 border-t border-[var(--line)]">
<div className="text-[11px] text-[var(--ink-muted)]"></div>
<select value={transferTo} onChange={(e) => setTransferTo(e.target.value)} className="h-8 w-full rounded-lg border border-[var(--line)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none">
<option value=""></option>
{members.filter((m) => m.name !== bug.assigneeId).map((m) => <option key={m.id} value={m.name}>{m.name}</option>)}
</select>
<input value={transferRemark} onChange={(e) => setTransferRemark(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" />
<div className="flex gap-2">
<button onClick={handleTransfer} disabled={!transferTo} className="h-8 px-3 rounded-lg text-[11px] font-medium bg-blue-500 text-white disabled:opacity-50"></button>
<button onClick={() => setShowTransfer(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, 16).replace('T', ' ')}</span></div>
{bug.resolvedAt && <div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{bug.resolvedAt.slice(0, 16).replace('T', ' ')}</span></div>}
{bug.closedAt && <div><span className="text-[var(--ink-muted)]"></span><span className="text-emerald-600">{bug.closedAt.slice(0, 16).replace('T', ' ')}</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.images && bug.images.length > 0 && (
<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"></div>
<div className="flex flex-wrap gap-2">
{bug.images.map((src, i) => (
<img key={i} src={src} alt={`截图${i + 1}`} className="h-24 w-24 object-cover rounded-lg border border-[var(--line)] cursor-pointer hover:ring-2 hover:ring-[var(--accent)]" onClick={() => setLightboxSrc(src)} />
))}
</div>
</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>
)}
{/* 操作日志 */}
{bug.logs && bug.logs.length > 0 && (
<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="space-y-2.5">
{[...bug.logs].reverse().map((log) => (
<div key={log.id} className="flex gap-2.5 text-[11px]">
<span className="text-[var(--ink-muted)] tabular-nums shrink-0 w-[110px]">{log.createdAt.slice(0, 16).replace('T', ' ')}</span>
<div className="flex-1">
<span className="font-medium text-[var(--ink)]">{log.operator}</span>
<span className="text-[var(--ink-soft)]"> {LOG_ACTION_LABEL[log.action] || log.action}</span>
{log.fromValue && log.toValue && (
<span className="text-[var(--ink-muted)]"> {log.fromValue} {log.toValue}</span>
)}
{log.remark && <span className="text-[var(--ink-muted)]"> ({log.remark})</span>}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Lightbox */}
{lightboxSrc && (
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/70" onClick={(e) => { e.stopPropagation(); setLightboxSrc(null); }}>
<img src={lightboxSrc} alt="大图预览" className="max-h-[85vh] max-w-[85vw] rounded-lg shadow-2xl" onClick={(e) => e.stopPropagation()} />
<button onClick={(e) => { e.stopPropagation(); setLightboxSrc(null); }} className="absolute top-4 right-4 h-8 w-8 flex items-center justify-center rounded-full bg-white/20 hover:bg-white/40 text-white">
<X className="h-5 w-5" />
</button>
</div>
)}
</div>
);
}