feat(测试用例): 展示预估和实际耗时

This commit is contained in:
Script Generator
2026-06-25 14:55:45 +08:00
parent 88acf30296
commit 5723356d08
4 changed files with 108 additions and 18 deletions

View File

@@ -1,12 +1,15 @@
'use client';
import { useState, useMemo } from 'react';
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 type { Priority } from '@/lib/derive';
import { getCategoriesByGroup, getDefaultCategoryByGroup } from '@/lib/task-category';
import { clampTestCaseEstimateHours, getDefaultTestCaseEstimateHours } from '@/lib/ai-estimation-policy';
interface Props {
versionId: string;
@@ -18,30 +21,66 @@ export function TestCaseCreateModal({ versionId, requirementIds, onClose }: Prop
const { createTestCase } = useTestCaseStore();
const { requirements } = useRequirementStore();
const { members } = useMemberStore();
const { categories } = useTaskCategoryStore();
const user = useAuthStore((s) => s.user);
const versionReqs = useMemo(
() => requirements.filter((r) => requirementIds.includes(r.id)),
[requirements, requirementIds],
);
const testCategories = useMemo(() => getCategoriesByGroup(categories, 'testing'), [categories]);
const [title, setTitle] = useState('');
const [requirementId, setRequirementId] = useState(versionReqs[0]?.id || '');
const [priority, setPriority] = useState<Priority>(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 [assigneeId, setAssigneeId] = useState(user?.name || '');
const [description, setDescription] = useState('');
const [prototypeNotes, setPrototypeNotes] = useState('');
const canSubmit = title.trim();
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]);
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 canSubmit = title.trim() && categoryId && normalizedEstimateHours > 0;
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,
title: title.trim(),
description: description.trim() || undefined,
categoryId,
priority,
estimateHours: normalizedEstimateHours,
assigneeId: assigneeId || undefined,
references: references.length > 0 ? references : undefined,
createdBy: user?.name || '系统',
});
onClose();
@@ -73,6 +112,27 @@ export function TestCaseCreateModal({ versionId, requirementIds, onClose }: Prop
{(['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={categoryId} onChange={(e) => handleCategoryChange(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">
{testCategories.map((category) => (
<option key={category.id} value={category.id}>{category.name}</option>
))}
</select>
</div>
<div>
<label className="block text-[12px] text-[var(--ink-soft)] mb-1"> *</label>
<input
type="number"
min="0.25"
max="2"
step="0.25"
value={estimateHours}
onChange={(e) => 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"
/>
</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">
@@ -85,6 +145,11 @@ export function TestCaseCreateModal({ versionId, requirementIds, onClose }: Prop
<label className="block text-[12px] text-[var(--ink-soft)] mb-1"> & </label>
<textarea rows={4} 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="1. 操作步骤...&#10;2. 预期结果..." />
</div>
<div>
<label className="block text-[12px] text-[var(--ink-soft)] mb-1"></label>
<input value={prototypeNotes} onChange={(e) => setPrototypeNotes(e.target.value)} className="h-8 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" placeholder="如 QY0007, QY0023" />
<p className="mt-1 text-[11px] text-[var(--ink-muted)]"></p>
</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>