'use client'; import { useState } from 'react'; import { Plus, Pencil, Trash2, X, Check, ExternalLink, FileUp, Link2, Play } from 'lucide-react'; import type { VersionPlan, PlanTask } from '@/lib/version-plan'; import { calcPlanDuration, formatDuration, calcTotalDuration, calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan'; interface Props { plans: VersionPlan[]; versionId: string; versionDeadline?: string; currentUserName: string; planType: 'research' | 'product' | 'ui'; linkedRequirements?: { id: string; title: string; code: string; productOwner?: string }[]; onCreate: (data: Omit) => void; onUpdate: (id: string, data: Partial) => void; onComplete: (id: string, result: { resultType: 'link' | 'file'; resultUrl?: string; resultFileName?: string; resultFileData?: string }) => void; onDelete: (id: string) => void; } const TYPE_LABEL = { research: '调研', product: '产品方案', ui: 'UI设计' }; const STATUS_STYLE = { pending: 'bg-zinc-100 text-zinc-600', in_progress: 'bg-blue-50 text-blue-600 border-blue-200', completed: 'bg-green-50 text-green-700 border-green-200', }; const STATUS_LABEL = { pending: '未开始', in_progress: '进行中', completed: '已完成' }; export function PlanTab({ plans, versionId, versionDeadline, currentUserName, planType, linkedRequirements, onCreate, onUpdate, onComplete, onDelete }: Props) { const [showCreateModal, setShowCreateModal] = useState(false); const [editingPlan, setEditingPlan] = useState(null); const [completingPlan, setCompletingPlan] = useState(null); const typePlans = plans.filter((p) => p.versionId === versionId && p.type === planType); const totalDuration = calcTotalDuration(typePlans); return (
{typePlans.length} 个{TYPE_LABEL[planType]}计划 已耗时:{totalDuration} {versionDeadline && 版本截止:{versionDeadline}}
{typePlans.length === 0 ? (
暂无{TYPE_LABEL[planType]}计划
) : (
{typePlans.map((plan) => { const now = new Date().toISOString(); // 耗时用实际时间戳计算 const dur = plan.status === 'completed' && plan.completedAt && plan.actualStartAt ? calcPlanDuration(plan.actualStartAt, plan.completedAt) : plan.status === 'in_progress' && plan.actualStartAt ? calcPlanDuration(plan.actualStartAt, now) : { days: 0, hours: 0 }; const durText = dur.days > 0 || dur.hours > 0 ? formatDuration(dur.days, dur.hours) : '-'; return (
{plan.title} {STATUS_LABEL[plan.status]}
计划:{plan.startTime.slice(0, 10)} → {plan.endTime.slice(0, 10)} {plan.actualStartAt && 实际开始:{plan.actualStartAt.slice(0, 16).replace('T', ' ')}} {plan.completedAt && 完成:{plan.completedAt.slice(0, 16).replace('T', ' ')}} 已耗时 {durText}
{plan.overdueReason && (
超期原因:{plan.overdueReason}
)} {(plan.type === 'product' || plan.type === 'ui') && (
负责人:{plan.owner}
)} {plan.status === 'completed' && plan.resultUrl && (
{plan.resultType === 'link' ? : } {plan.resultFileName || '查看成果'}
)} {/* 调研:任务进度 */} {plan.type === 'research' && plan.tasks && plan.tasks.length > 0 && (
{calcPlanProgress(plan.tasks)}%
{plan.tasks.map((task) => (
{task.title} {task.status === 'completed' ? '已完成' : task.status === 'in_progress' ? '进行中' : '未开始'}
))}
)} {/* 产品方案/UI:关联需求进度 */} {(plan.type === 'product' || plan.type === 'ui') && plan.linkedRequirementIds && plan.linkedRequirementIds.length > 0 && linkedRequirements && (
{calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds)}%
{plan.linkedRequirementIds.map((rid) => { const req = linkedRequirements.find((r) => r.id === rid); const isDone = (plan.completedRequirementIds || []).includes(rid); return req ? (
{req.code} {req.title}
) : null; })}
{calcLinkedReqProgress(plan.linkedRequirementIds, plan.completedRequirementIds) === 100 && plan.status !== 'completed' && (
所有需求已完成,请提交原型成果
)}
)}
{plan.status === 'pending' && ( )} {plan.status !== 'completed' && plan.status !== 'pending' && ( )} {plan.status !== 'completed' && ( <> )}
); })}
)} {(showCreateModal || editingPlan) && ( { setShowCreateModal(false); setEditingPlan(null); }} onSubmit={(data) => { if (editingPlan) onUpdate(editingPlan.id, data); else onCreate(data as any); setShowCreateModal(false); setEditingPlan(null); }} /> )} {completingPlan && ( setCompletingPlan(null)} onSubmit={(result) => { onComplete(completingPlan.id, result); setCompletingPlan(null); }} /> )}
); } function PlanFormModal({ initial, planType, versionId, versionDeadline, currentUserName, linkedRequirements, onClose, onSubmit }: { initial: VersionPlan | null; planType: 'research' | 'product' | 'ui'; versionId: string; versionDeadline?: string; currentUserName: string; linkedRequirements?: { id: string; title: string; code: string }[]; onClose: () => void; onSubmit: (data: any) => void; }) { const now = new Date().toISOString().slice(0, 10); const [title, setTitle] = useState(initial?.title ?? ''); const [owner] = useState(initial?.owner ?? currentUserName); const [startTime, setStartTime] = useState(initial?.startTime?.slice(0, 10) ?? now); const [endTime, setEndTime] = useState(initial?.endTime?.slice(0, 10) ?? ''); const [remark, setRemark] = useState(initial?.remark ?? ''); const [tasks, setTasks] = useState(initial?.tasks ?? []); const [newTaskTitle, setNewTaskTitle] = useState(''); const [overdueReason, setOverdueReason] = useState(initial?.overdueReason ?? ''); const [selectedReqs, setSelectedReqs] = useState>(new Set(initial?.linkedRequirementIds ?? [])); const showReqSelect = planType === 'product' || planType === 'ui'; const isOverdue = !!(versionDeadline && endTime && new Date(endTime) > new Date(versionDeadline)); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!title.trim() || !endTime) return; if (isOverdue && !overdueReason.trim()) return; onSubmit({ versionId, type: planType, title: title.trim(), owner: owner.trim() || currentUserName, startTime, endTime, status: initial?.status ?? 'pending', linkedRequirementIds: showReqSelect ? Array.from(selectedReqs) : undefined, tasks: tasks.length > 0 ? tasks : undefined, remark: remark.trim() || undefined, overdueReason: isOverdue ? overdueReason.trim() : undefined, addedBy: currentUserName, }); }; return (
e.stopPropagation()}>

{initial ? '编辑' : '新建'}{TYPE_LABEL[planType]}计划

setTitle(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
setStartTime(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
setEndTime(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
{versionDeadline && (
版本截止日期:{versionDeadline}
)} {isOverdue && (
⚠ 结束日期超出版本截止日期,请说明原因