Files
ftb-project-management/apps/web/components/bug/BugCreateModal.tsx
Script Generator 93f6c33109 refactor: 压平数据模型 — 版本为执行主线,需求降为语义标签
核心改动:
- TestCase 主归属 versionId,requirementId 改为可选语义标签
- Bug 新增 versionId(直接挂版本)+ requirementId(可选追溯)
- 新增 calcVersionExecutionStatus() 版本执行态推导函数
  规则:DevTask全部提测→已提测,TestCase全部通过+Bug全关→可发布
- 概览页展示推导出的执行态标签
- BugTab 改为直接 versionId 查询(不再走 testCase→req 链路)
- 统计卡片"参与人员"替换为"未关闭Bug"

心智模型:
  版本(执行线)→ DevTask/TestCase/Bug
  需求(语义线)→ 解释为什么做,不驱动流程

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 14:07:46 +08:00

120 lines
6.4 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({
versionId: tc?.versionId || '',
testCaseId,
requirementId: tc?.requirementId || undefined,
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>
);
}