feat: 计划转交功能 + 关联需求增强(描述/变更/类型/日期)

一、调研/产品方案/UI设计 - 转交功能:
- 未开始/进行中状态可转交给其他参与人员
- 转交按钮打开内联选择器,从 version.members 选人
- 确认后更新 plan.owner

二、关联需求表格增强:
- 描述列:超出宽度省略号,hover 显示完整 title 属性
- 需求类型列:原始需求(灰色)/ 变更需求(橙色)
- 变更原因列:显示变更原因标签
- 添加日期列:显示 createdAt

三、需求变更功能:
- 新增"需求变更"按钮(橙色边框)
- 变更表单:变更人员(从参与人员选)、变更原因(6种)、
  需求概述、需求详细
- 变更原因选项:需求理解偏差导致返工、反馈导致返工、
  实现难度超预期、上游交付延误、范围蔓延、其他
- 创建后自动关联到当前版本,标记为变更需求

四、数据模型扩展(lib/requirement.ts):
- 新增 reqType: 'original' | 'change'
- 新增 changeReason: ChangeReason
- 新增 changeBy: string
- 导出 CHANGE_REASON_LABEL 常量

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Script Generator
2026-06-15 16:21:14 +08:00
parent 54efe53b11
commit 700c8a9aba
4 changed files with 158 additions and 10 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { Plus, Pencil, Trash2, X, Check, ExternalLink, FileUp, Link2, Play } from 'lucide-react';
import { Plus, Pencil, Trash2, X, Check, ExternalLink, FileUp, Link2, Play, ArrowRightLeft } from 'lucide-react';
import type { VersionPlan, PlanTask } from '@/lib/version-plan';
import { calcPlanDuration, formatDuration, calcTotalDuration, calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan';
@@ -11,6 +11,7 @@ interface Props {
versionDeadline?: string;
currentUserName: string;
planType: 'research' | 'product' | 'ui';
versionMembers: { role: string; name: string }[];
linkedRequirements?: { id: string; title: string; code: string; productOwner?: string }[];
onCreate: (data: Omit<VersionPlan, 'id' | 'createdAt'>) => void;
onUpdate: (id: string, data: Partial<VersionPlan>) => void;
@@ -26,10 +27,12 @@ const STATUS_STYLE = {
};
const STATUS_LABEL = { pending: '未开始', in_progress: '进行中', completed: '已完成' };
export function PlanTab({ plans, versionId, versionDeadline, currentUserName, planType, linkedRequirements, onCreate, onUpdate, onComplete, onDelete }: Props) {
export function PlanTab({ plans, versionId, versionDeadline, currentUserName, planType, versionMembers, linkedRequirements, onCreate, onUpdate, onComplete, onDelete }: Props) {
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingPlan, setEditingPlan] = useState<VersionPlan | null>(null);
const [completingPlan, setCompletingPlan] = useState<VersionPlan | null>(null);
const [transferPlanId, setTransferPlanId] = useState<string | null>(null);
const [transferTo, setTransferTo] = useState('');
const typePlans = plans.filter((p) => p.versionId === versionId && p.type === planType);
const totalDuration = calcTotalDuration(typePlans);
@@ -189,6 +192,9 @@ export function PlanTab({ plans, versionId, versionDeadline, currentUserName, pl
)}
{plan.status !== 'completed' && (
<>
<button onClick={() => setTransferPlanId(plan.id)} className="h-7 w-7 flex items-center justify-center rounded-md text-blue-500 hover:bg-blue-50" title="转交">
<ArrowRightLeft className="h-3.5 w-3.5" />
</button>
<button onClick={() => setEditingPlan(plan)} className="h-7 w-7 flex items-center justify-center rounded-md text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)]" title="编辑">
<Pencil className="h-3.5 w-3.5" />
</button>
@@ -199,6 +205,17 @@ export function PlanTab({ plans, versionId, versionDeadline, currentUserName, pl
)}
</div>
</div>
{transferPlanId === plan.id && (
<div className="mt-3 pt-3 border-t border-[var(--line)] flex items-center gap-2">
<span className="text-[11px] text-[var(--ink-muted)]"></span>
<select value={transferTo} onChange={(e) => setTransferTo(e.target.value)} className="h-7 flex-1 rounded-lg border border-[var(--line)] px-2 text-[12px] focus:border-[var(--accent)] focus:outline-none">
<option value=""></option>
{versionMembers.filter((m) => m.name !== plan.owner).map((m) => <option key={`${m.role}-${m.name}`} value={m.name}>{m.name}</option>)}
</select>
<button onClick={() => { if (transferTo) { onUpdate(plan.id, { owner: transferTo }); setTransferPlanId(null); setTransferTo(''); } }} disabled={!transferTo} className="h-7 px-2.5 rounded-lg text-[11px] font-medium bg-blue-500 text-white disabled:opacity-50"></button>
<button onClick={() => { setTransferPlanId(null); setTransferTo(''); }} className="h-7 px-2 text-[11px] text-[var(--ink-muted)]"></button>
</div>
)}
</div>
);
})}