feat(测试用例): 展示预估和实际耗时
This commit is contained in:
@@ -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. 操作步骤... 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>
|
||||
|
||||
Reference in New Issue
Block a user