From 700c8a9aba9b4394343025724daf32adeff07e6e Mon Sep 17 00:00:00 2001 From: Script Generator Date: Mon, 15 Jun 2026 16:21:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A1=E5=88=92=E8=BD=AC=E4=BA=A4?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20+=20=E5=85=B3=E8=81=94=E9=9C=80=E6=B1=82?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=EF=BC=88=E6=8F=8F=E8=BF=B0/=E5=8F=98?= =?UTF-8?q?=E6=9B=B4/=E7=B1=BB=E5=9E=8B/=E6=97=A5=E6=9C=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、调研/产品方案/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) --- apps/web/app/versions/[id]/page.tsx | 22 +++- apps/web/components/version/PlanTab.tsx | 21 +++- .../version/VersionRequirementsTab.tsx | 111 ++++++++++++++++-- apps/web/lib/requirement.ts | 14 +++ 4 files changed, 158 insertions(+), 10 deletions(-) diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index 575b155..02b38df 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -52,7 +52,7 @@ export default function VersionDetailPage() { const router = useRouter(); const versionId = params.id as string; const { overview, fetchOverview, updateVersion, deleteVersion } = useProductStore(); - const { requirements, fetchRequirements, updateRequirement } = useRequirementStore(); + const { requirements, fetchRequirements, updateRequirement, createRequirement } = useRequirementStore(); const { records, fetchRecords } = useOvertimeStore(); const { plans, fetchPlans, createPlan, updatePlan, completePlan, deletePlan } = useVersionPlanStore(); const { tasks: devTasks, fetchTasks: fetchDevTasks, deleteTask: deleteDevTask } = useDevTaskStore(); @@ -751,11 +751,30 @@ export default function VersionDetailPage() { projectId={version.projectId} requirements={requirements} devTasks={devTasks} + versionMembers={version.members ?? []} currentUserName={user?.name ?? ''} onLink={(ids, addedBy) => { ids.forEach((id) => updateRequirement(id, { versionId: version.id, addedToVersionBy: addedBy })); }} onUnlink={(id) => updateRequirement(id, { versionId: undefined, addedToVersionBy: undefined })} + onCreateChange={(data) => { + createRequirement({ + ...data, + productId: version.productId, + projectId: version.projectId, + versionId: version.id, + sourceType: 'internal', + sourceTarget: '', + platforms: [], + typeId: '', + status: 'adopted', + priority: 'P2', + effort: 'M', + creator: user?.name ?? '', + addedToVersionBy: user?.name ?? '', + reqType: 'change', + } as any); + }} /> ) : (activeTab === 'research' || activeTab === 'product' || activeTab === 'ui') ? ( (() => { @@ -769,6 +788,7 @@ export default function VersionDetailPage() { versionDeadline={version.expectedReleaseDate ?? undefined} currentUserName={user?.name ?? ''} planType={pt} + versionMembers={version.members ?? []} linkedRequirements={pt !== 'research' ? linkedReqs : undefined} onCreate={(data) => { createPlan(data); diff --git a/apps/web/components/version/PlanTab.tsx b/apps/web/components/version/PlanTab.tsx index ffb9890..115e395 100644 --- a/apps/web/components/version/PlanTab.tsx +++ b/apps/web/components/version/PlanTab.tsx @@ -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) => void; onUpdate: (id: string, data: Partial) => 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(null); const [completingPlan, setCompletingPlan] = useState(null); + const [transferPlanId, setTransferPlanId] = useState(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' && ( <> + @@ -199,6 +205,17 @@ export function PlanTab({ plans, versionId, versionDeadline, currentUserName, pl )} + {transferPlanId === plan.id && ( +
+ 转交给: + + + +
+ )} ); })} diff --git a/apps/web/components/version/VersionRequirementsTab.tsx b/apps/web/components/version/VersionRequirementsTab.tsx index 557455c..7eaf36e 100644 --- a/apps/web/components/version/VersionRequirementsTab.tsx +++ b/apps/web/components/version/VersionRequirementsTab.tsx @@ -2,9 +2,9 @@ import { useState } from 'react'; import { Plus, X, Search } from 'lucide-react'; -import type { Requirement } from '@/lib/requirement'; +import type { Requirement, ChangeReason } from '@/lib/requirement'; import type { DevTask } from '@/lib/dev-task'; -import { REQ_STATUS_LABEL, REQ_STATUS_COLOR } from '@/lib/requirement'; +import { REQ_STATUS_LABEL, REQ_STATUS_COLOR, CHANGE_REASON_LABEL } from '@/lib/requirement'; import { deriveReqDevStatus, canEditRequirement, REQ_DEV_STATUS_LABEL, REQ_DEV_STATUS_COLOR } from '@/lib/linkage-engine'; interface Props { @@ -12,13 +12,16 @@ interface Props { projectId: string; requirements: Requirement[]; devTasks: DevTask[]; + versionMembers: { role: string; name: string }[]; onLink: (reqIds: string[], addedBy: string) => void; onUnlink: (reqId: string) => void; + onCreateChange: (data: Partial) => void; currentUserName: string; } -export function VersionRequirementsTab({ versionId, projectId, requirements, devTasks, onLink, onUnlink, currentUserName }: Props) { +export function VersionRequirementsTab({ versionId, projectId, requirements, devTasks, versionMembers, onLink, onUnlink, onCreateChange, currentUserName }: Props) { const [showAddModal, setShowAddModal] = useState(false); + const [showChangeModal, setShowChangeModal] = useState(false); const linkedReqs = requirements.filter((r) => r.versionId === versionId); const availableReqs = requirements.filter((r) => r.projectId === projectId && !r.versionId && r.status === 'adopted'); @@ -26,10 +29,16 @@ export function VersionRequirementsTab({ versionId, projectId, requirements, dev
{linkedReqs.length} 条关联需求 - +
+ + +
{linkedReqs.length === 0 ? ( @@ -43,9 +52,13 @@ export function VersionRequirementsTab({ versionId, projectId, requirements, dev 编号 需求概述 + 描述 + 需求类型 + 变更原因 状态 开发状态 添加人 + 添加日期 操作 @@ -57,6 +70,17 @@ export function VersionRequirementsTab({ versionId, projectId, requirements, dev {req.code} {req.title} + + {req.description || '-'} + + + + {req.reqType === 'change' ? '变更需求' : '原始需求'} + + + + {req.changeReason ? CHANGE_REASON_LABEL[req.changeReason] : '-'} + {REQ_STATUS_LABEL[req.status]} @@ -68,6 +92,7 @@ export function VersionRequirementsTab({ versionId, projectId, requirements, dev {req.addedToVersionBy || '系统添加'} + {req.createdAt?.slice(0, 10) || '-'} {isDeveloping ? ( 开发中 @@ -90,6 +115,78 @@ export function VersionRequirementsTab({ versionId, projectId, requirements, dev onConfirm={(ids) => { onLink(ids, currentUserName); setShowAddModal(false); }} /> )} + + {showChangeModal && ( + setShowChangeModal(false)} + onSubmit={(data) => { onCreateChange(data); setShowChangeModal(false); }} + /> + )} +
+ ); +} + +function ChangeRequirementModal({ versionMembers, currentUserName, onClose, onSubmit }: { + versionMembers: { role: string; name: string }[]; + currentUserName: string; + onClose: () => void; + onSubmit: (data: Partial) => void; +}) { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [changeBy, setChangeBy] = useState(currentUserName); + const [changeReason, setChangeReason] = useState(''); + + const handleSubmit = () => { + if (!title.trim() || !changeReason || !changeBy) return; + onSubmit({ + title: title.trim(), + description: description.trim(), + changeBy, + changeReason: changeReason as ChangeReason, + reqType: 'change', + }); + }; + + return ( +
+
e.stopPropagation()}> +
+

需求变更

+ +
+
+
+ + +
+
+ + +
+
+ + setTitle(e.target.value)} placeholder="变更需求标题" 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" /> +
+
+ +