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} 次
+
+ ))}
+
+ )}
+
+
+
变更原因占比
+
+
+
+ {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 = {