Files
ftb-project-management/apps/web/components/bug/BugCreateModal.tsx
Script Generator 7fe3a3854a 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>
2026-06-12 13:11:08 +08:00

118 lines
6.3 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, 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<BugSeverity>('major');
const [priority, setPriority] = useState<Priority>(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 (
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/40" onClick={onClose}>
<div className="w-full max-w-md rounded-2xl bg-[var(--bg-card)] border border-[var(--line)] p-6 shadow-[var(--shadow-md)]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-[14px] font-semibold text-[var(--ink)]"> Bug</h3>
<button onClick={onClose} className="rounded-md p-1 hover:bg-[var(--bg-subtle)]"><X className="h-4 w-4 text-[var(--ink-muted)]" /></button>
</div>
{/* 只读关联信息 */}
<div className="rounded-lg bg-[var(--bg-subtle)] px-3 py-2 mb-4 space-y-1 text-[12px]">
<div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{tc?.caseNo} {tc?.title}</span></div>
{requirement && <div><span className="text-[var(--ink-muted)]"></span><span className="text-[var(--ink)]">{requirement.code} {requirement.title}</span></div>}
</div>
<div className="space-y-3">
<div>
<label className="block text-[12px] text-[var(--ink-soft)] mb-1">Bug *</label>
<input value={title} onChange={(e) => 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="简要描述问题" />
</div>
<div>
<label className="block text-[12px] text-[var(--ink-soft)] mb-1">Bug *</label>
<textarea rows={3} value={description} onChange={(e) => setDescription(e.target.value)} className="w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 py-2 text-[13px] focus:border-[var(--accent)] focus:outline-none resize-none" placeholder="复现步骤、实际结果、预期结果" />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-[12px] text-[var(--ink-soft)] mb-1"></label>
<select value={severity} onChange={(e) => setSeverity(e.target.value as BugSeverity)} 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">
<option value="critical"></option>
<option value="major"></option>
<option value="minor"></option>
<option value="trivial"></option>
</select>
</div>
<div>
<label className="block text-[12px] text-[var(--ink-soft)] mb-1"></label>
<select value={priority} onChange={(e) => setPriority(e.target.value as Priority)} 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">
{(['P0','P1','P2','P3'] as Priority[]).map((p) => <option key={p} value={p}>{p}</option>)}
</select>
</div>
<div>
<label className="block text-[12px] text-[var(--ink-soft)] mb-1"> *</label>
<select value={assigneeId} onChange={(e) => setAssigneeId(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">
<option value=""></option>
{members.map((m) => <option key={m.id} value={m.name}>{m.name}</option>)}
</select>
</div>
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t border-[var(--line)] mt-4">
<button onClick={onClose} className="h-8 px-3 rounded-lg text-[12px] font-medium border border-[var(--line)] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)]"></button>
<button onClick={handleSubmit} disabled={!canSubmit} className="h-8 px-4 rounded-lg text-[12px] font-medium bg-red-500 text-white hover:bg-red-600 disabled:opacity-50"> Bug</button>
</div>
</div>
</div>
);
}