From 9c310534e638869cef0f2bd50b27235d64ca5048 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Mon, 15 Jun 2026 16:28:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8E=BB=E6=8E=89=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E8=94=93=E5=BB=B6=E5=92=8C=E5=85=B6=E4=BB=96=E9=80=89=E9=A1=B9?= =?UTF-8?q?=20+=20=E6=A6=82=E8=A7=88=E5=A2=9E=E5=8A=A0=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 变更原因精简为4个: - 需求理解偏差导致返工 - 反馈导致返工 - 实现难度超预期 - 上游交付延误 去掉"范围蔓延"和"其他"(避免大家逃避问题选其他) 2. 顶部小卡片新增"需求变更"计数(橙色警告色) 3. 概览新增变更统计区域(加班时长下方): - 变更人员排名:谁发起的变更次数最多 - 变更原因占比:环形饼图 + 图例百分比 - 无变更时不显示此区域 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/app/versions/[id]/page.tsx | 82 ++++++++++++++++++++++++++++- apps/web/lib/requirement.ts | 4 +- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index 02b38df..3f5191f 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -13,7 +13,7 @@ import { CapsuleStages } from '@/components/version/CapsuleStages'; import { MemberChips } from '@/components/version/MemberChips'; import { HealthTrend, generateMockTrend } from '@/components/version/HealthTrend'; import { calcHealthScore, getHealthLevel, calcRiskTags, HEALTH_LEVEL_COLOR, HEALTH_LEVEL_DOT, HEALTH_LEVEL_LABEL, getTagStyle } from '@/lib/health'; -import { REQ_STATUS_LABEL, REQ_STATUS_COLOR } from '@/lib/requirement'; +import { REQ_STATUS_LABEL, REQ_STATUS_COLOR, CHANGE_REASON_LABEL } from '@/lib/requirement'; import { OVERTIME_REASON_LABEL } from '@/lib/overtime'; import { VersionRequirementsTab } from '@/components/version/VersionRequirementsTab'; import { PlanTab } from '@/components/version/PlanTab'; @@ -285,8 +285,9 @@ export default function VersionDetailPage() { const pendingResearch = vPlans.filter((p) => p.type === 'research' && p.status !== 'completed').length; const pendingProduct = vPlans.filter((p) => p.type === 'product' && p.status !== 'completed').length; return ( -
+
+ r.reqType === 'change').length} warn={versionReqs.filter((r) => r.reqType === 'change').length > 0} /> 0} /> 0} /> @@ -586,6 +587,83 @@ export default function VersionDetailPage() {
+ {/* 6. 需求变更统计 */} + {(() => { + const changeReqs = requirements.filter((r) => r.versionId === version.id && r.reqType === 'change'); + if (changeReqs.length === 0) return null; + + // 变更人员排名 + const personChange: Record = {}; + changeReqs.forEach((r) => { const by = r.changeBy || r.creator; personChange[by] = (personChange[by] || 0) + 1; }); + const changePersonRanking = Object.entries(personChange).sort((a, b) => b[1] - a[1]); + + // 变更原因占比 + const reasonCount: Record = {}; + changeReqs.forEach((r) => { if (r.changeReason) reasonCount[r.changeReason] = (reasonCount[r.changeReason] || 0) + 1; }); + const changeReasonRanking = Object.entries(reasonCount).sort((a, b) => b[1] - a[1]); + const reasonTotal = changeReqs.length; + + const colors = ['#f97316', '#3b82f6', '#8b5cf6', '#10b981']; + + return ( +
+
+
变更人员排名
+ {changePersonRanking.length === 0 ? ( + 暂无数据 + ) : ( +
+ {changePersonRanking.map(([name, count], i) => ( +
+
+ {i + 1} + {name} +
+ {count} 次 +
+ ))} +
+ )} +
+
+
变更原因占比
+
+ + {(() => { + let cumPercent = 0; + return changeReasonRanking.map(([reasonId, count], i) => { + const percent = count / reasonTotal; + const startAngle = cumPercent * 360; + cumPercent += percent; + const endAngle = cumPercent * 360; + const largeArc = percent > 0.5 ? 1 : 0; + const startRad = ((startAngle - 90) * Math.PI) / 180; + const endRad = ((endAngle - 90) * Math.PI) / 180; + const x1 = 50 + 40 * Math.cos(startRad); + const y1 = 50 + 40 * Math.sin(startRad); + const x2 = 50 + 40 * Math.cos(endRad); + const y2 = 50 + 40 * Math.sin(endRad); + if (percent >= 1) return ; + return ; + }); + })()} + + +
+ {changeReasonRanking.map(([reasonId, count], i) => ( +
+ + {CHANGE_REASON_LABEL[reasonId as keyof typeof CHANGE_REASON_LABEL] || reasonId} + {Math.round((count / reasonTotal) * 100)}% +
+ ))} +
+
+
+
+ ); + })()} + {/* 阶段耗时 + 个人耗时排名 */} {(() => { const versionPlans = plans.filter((p) => p.versionId === version.id); diff --git a/apps/web/lib/requirement.ts b/apps/web/lib/requirement.ts index 98b4a36..7afa210 100644 --- a/apps/web/lib/requirement.ts +++ b/apps/web/lib/requirement.ts @@ -43,15 +43,13 @@ export interface Requirement { changeBy?: string; } -export type ChangeReason = 'misunderstanding' | 'feedback_rework' | 'difficulty_exceeded' | 'upstream_delay' | 'scope_creep' | 'other'; +export type ChangeReason = 'misunderstanding' | 'feedback_rework' | 'difficulty_exceeded' | 'upstream_delay'; export const CHANGE_REASON_LABEL: Record = { misunderstanding: '需求理解偏差导致返工', feedback_rework: '反馈导致返工', difficulty_exceeded: '实现难度超预期', upstream_delay: '上游交付延误', - scope_creep: '范围蔓延', - other: '其他', }; export const SOURCE_TYPE_LABEL: Record = {