'use client'; import { useEffect, useState, useMemo } from 'react'; import { X } from 'lucide-react'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { FilterSelect } from '@/components/FilterSelect'; import { WorkDateTimePicker } from '@/components/WorkDateTimePicker'; import type { Priority } from '@/lib/derive'; import type { Requirement } from '@/lib/requirement'; import { getCategoriesByGroup, getDefaultCategoryByGroup } from '@/lib/task-category'; import { getVersionLinkedRequirementCandidates } from '@/lib/requirement-selector'; import { clampTestCaseEstimateHours, getDefaultTestCaseEstimateHours } from '@/lib/ai-estimation-policy'; import { isoToLocal, localToISO } from '@/lib/work-hours'; interface Props { versionId: string; requirementIds: string[]; versionRequirements?: Requirement[]; roundNo?: number; onClose: () => void; } function defaultPlannedTestLocal(): string { const d = new Date(); d.setHours(9, 0, 0, 0); return isoToLocal(d.toISOString()); } function defaultPlannedEndLocal(): string { const d = new Date(); d.setHours(10, 0, 0, 0); return isoToLocal(d.toISOString()); } export function TestCaseCreateModal({ versionId, requirementIds, versionRequirements, roundNo, onClose }: Props) { const { createTestCase } = useTestCaseStore(); const { requirements } = useRequirementStore(); const { members } = useMemberStore(); const { categories } = useTaskCategoryStore(); const user = useAuthStore((s) => s.user); const requirementSource = versionRequirements ?? requirements; const versionReqs = useMemo( () => getVersionLinkedRequirementCandidates(requirementSource, requirementIds), [requirementSource, requirementIds], ); const testCategories = useMemo(() => getCategoriesByGroup(categories, 'testing'), [categories]); const [title, setTitle] = useState(''); const [requirementId, setRequirementId] = useState(versionReqs[0]?.id || ''); const [priority, setPriority] = useState(versionReqs[0]?.priority || 'P2'); const [categoryId, setCategoryId] = useState(getDefaultCategoryByGroup(categories, 'testing').id); const selectedCategory = useMemo( () => categories.find((category) => category.id === categoryId) ?? testCategories[0], [categories, categoryId, testCategories], ); const [estimateHours, setEstimateHours] = useState(0.5); const [plannedTestLocal, setPlannedTestLocal] = useState(defaultPlannedTestLocal); const [plannedEndLocal, setPlannedEndLocal] = useState(defaultPlannedEndLocal); const [assigneeId, setAssigneeId] = useState(user?.name || ''); const [description, setDescription] = useState(''); const [prototypeNotes, setPrototypeNotes] = useState(''); useEffect(() => { if (testCategories.length === 0) return; if (testCategories.some((category) => category.id === categoryId)) return; const next = testCategories[0]; setCategoryId(next.id); setEstimateHours(getDefaultTestCaseEstimateHours(next.code)); }, [categoryId, testCategories]); useEffect(() => { if (!requirementId) return; if (versionReqs.some((r) => r.id === requirementId)) return; setRequirementId(''); }, [versionReqs, requirementId]); const handleCategoryChange = (nextCategoryId: string) => { setCategoryId(nextCategoryId); const category = categories.find((c) => c.id === nextCategoryId) ?? testCategories.find((c) => c.id === nextCategoryId); setEstimateHours(getDefaultTestCaseEstimateHours(category?.code)); }; const normalizedEstimateHours = clampTestCaseEstimateHours(selectedCategory?.code, estimateHours); const plannedTestAt = localToISO(plannedTestLocal); const plannedEndAt = localToISO(plannedEndLocal); const hasValidPlan = Boolean(plannedTestAt && plannedEndAt && plannedEndAt > plannedTestAt); const canSubmit = title.trim() && categoryId && normalizedEstimateHours > 0 && hasValidPlan; const handleSubmit = () => { if (!canSubmit) return; const reqRef = versionReqs.find((r) => r.id === requirementId); const references = [ ...(reqRef ? [{ type: 'requirement' as const, id: reqRef.code, label: `${reqRef.code} ${reqRef.title}` }] : []), ...prototypeNotes.split(/[,,\s]+/).map((s) => s.trim()).filter(Boolean).map((note) => ({ type: 'prototype_note' as const, id: note, label: note, })), ]; createTestCase({ versionId, requirementId: requirementId || undefined, roundNo: roundNo ?? 1, title: title.trim(), description: description.trim() || undefined, categoryId, priority, estimateHours: normalizedEstimateHours, plannedTestAt, plannedEndAt, assigneeId: assigneeId || undefined, references: references.length > 0 ? references : undefined, createdBy: user?.name || '系统', }); onClose(); }; return (
e.stopPropagation()}>

新建测试用例

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="例如:正常排班" />
{ const nextValue = value === 'all' ? '' : value; setRequirementId(nextValue); const r = versionReqs.find((x) => x.id === nextValue); if (r) setPriority(r.priority); }} options={versionReqs.map((r) => ({ value: r.id, label: `${r.code} ${r.title}` }))} allLabel="不关联需求" className="w-full" labelClassName="max-w-[calc(100%-20px)]" />
setPriority(value as Priority)} options={(['P0', 'P1', 'P2', 'P3'] as Priority[]).map((p) => ({ value: p, label: p }))} showAllOption={false} className="w-full" labelClassName="max-w-[calc(100%-20px)]" />
({ value: category.id, label: category.name }))} placeholder="选择任务类型" showAllOption={false} className="w-full" labelClassName="max-w-[calc(100%-20px)]" />
setEstimateHours(Number(e.target.value))} onBlur={() => setEstimateHours(normalizedEstimateHours)} 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" />
setAssigneeId(value === 'all' ? '' : value)} options={members.map((m) => ({ value: m.name, label: m.name }))} allLabel="选择负责人" className="w-full" labelClassName="max-w-[calc(100%-20px)]" />
{!hasValidPlan && plannedTestLocal && plannedEndLocal && (
计划开始必须早于计划结束
)}