fix: 去掉范围蔓延和其他选项 + 概览增加变更统计
1. 变更原因精简为4个: - 需求理解偏差导致返工 - 反馈导致返工 - 实现难度超预期 - 上游交付延误 去掉"范围蔓延"和"其他"(避免大家逃避问题选其他) 2. 顶部小卡片新增"需求变更"计数(橙色警告色) 3. 概览新增变更统计区域(加班时长下方): - 变更人员排名:谁发起的变更次数最多 - 变更原因占比:环形饼图 + 图例百分比 - 无变更时不显示此区域 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import { CapsuleStages } from '@/components/version/CapsuleStages';
|
|||||||
import { MemberChips } from '@/components/version/MemberChips';
|
import { MemberChips } from '@/components/version/MemberChips';
|
||||||
import { HealthTrend, generateMockTrend } from '@/components/version/HealthTrend';
|
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 { 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 { OVERTIME_REASON_LABEL } from '@/lib/overtime';
|
||||||
import { VersionRequirementsTab } from '@/components/version/VersionRequirementsTab';
|
import { VersionRequirementsTab } from '@/components/version/VersionRequirementsTab';
|
||||||
import { PlanTab } from '@/components/version/PlanTab';
|
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 pendingResearch = vPlans.filter((p) => p.type === 'research' && p.status !== 'completed').length;
|
||||||
const pendingProduct = vPlans.filter((p) => p.type === 'product' && p.status !== 'completed').length;
|
const pendingProduct = vPlans.filter((p) => p.type === 'product' && p.status !== 'completed').length;
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-4 gap-3 sm:grid-cols-7">
|
<div className="grid grid-cols-4 gap-3 sm:grid-cols-8">
|
||||||
<StatCard label="关联需求" value={versionReqs.length} />
|
<StatCard label="关联需求" value={versionReqs.length} />
|
||||||
|
<StatCard label="需求变更" value={versionReqs.filter((r) => r.reqType === 'change').length} warn={versionReqs.filter((r) => r.reqType === 'change').length > 0} />
|
||||||
<StatCard label="待调研" value={pendingResearch} warn={pendingResearch > 0} />
|
<StatCard label="待调研" value={pendingResearch} warn={pendingResearch > 0} />
|
||||||
<StatCard label="待方案设计" value={pendingProduct} warn={pendingProduct > 0} />
|
<StatCard label="待方案设计" value={pendingProduct} warn={pendingProduct > 0} />
|
||||||
<StatCard label="待开发" value={devTaskTodo} />
|
<StatCard label="待开发" value={devTaskTodo} />
|
||||||
@@ -586,6 +587,83 @@ export default function VersionDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 6. 需求变更统计 */}
|
||||||
|
{(() => {
|
||||||
|
const changeReqs = requirements.filter((r) => r.versionId === version.id && r.reqType === 'change');
|
||||||
|
if (changeReqs.length === 0) return null;
|
||||||
|
|
||||||
|
// 变更人员排名
|
||||||
|
const personChange: Record<string, number> = {};
|
||||||
|
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<string, number> = {};
|
||||||
|
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 (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||||
|
<div className="text-[11px] text-[var(--ink-muted)] mb-3 font-medium">变更人员排名</div>
|
||||||
|
{changePersonRanking.length === 0 ? (
|
||||||
|
<span className="text-[12px] text-[var(--ink-muted)]">暂无数据</span>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{changePersonRanking.map(([name, count], i) => (
|
||||||
|
<div key={name} className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[11px] font-medium text-[var(--ink-muted)] w-4">{i + 1}</span>
|
||||||
|
<span className="text-[12px] text-[var(--ink)]">{name}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[12px] font-medium tabular-nums text-orange-600">{count} 次</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
|
||||||
|
<div className="text-[11px] text-[var(--ink-muted)] mb-3 font-medium">变更原因占比</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<svg viewBox="0 0 100 100" className="h-24 w-24 shrink-0">
|
||||||
|
{(() => {
|
||||||
|
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 <circle key={reasonId} cx="50" cy="50" r="40" fill={colors[i % colors.length]} />;
|
||||||
|
return <path key={reasonId} d={`M 50 50 L ${x1} ${y1} A 40 40 0 ${largeArc} 1 ${x2} ${y2} Z`} fill={colors[i % colors.length]} />;
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
<circle cx="50" cy="50" r="20" fill="var(--bg-card)" />
|
||||||
|
</svg>
|
||||||
|
<div className="space-y-1.5 flex-1">
|
||||||
|
{changeReasonRanking.map(([reasonId, count], i) => (
|
||||||
|
<div key={reasonId} className="flex items-center gap-2 text-[11px]">
|
||||||
|
<span className="h-2.5 w-2.5 rounded-sm shrink-0" style={{ backgroundColor: colors[i % colors.length] }} />
|
||||||
|
<span className="flex-1 text-[var(--ink-soft)] truncate">{CHANGE_REASON_LABEL[reasonId as keyof typeof CHANGE_REASON_LABEL] || reasonId}</span>
|
||||||
|
<span className="tabular-nums text-[var(--ink-muted)]">{Math.round((count / reasonTotal) * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* 阶段耗时 + 个人耗时排名 */}
|
{/* 阶段耗时 + 个人耗时排名 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const versionPlans = plans.filter((p) => p.versionId === version.id);
|
const versionPlans = plans.filter((p) => p.versionId === version.id);
|
||||||
|
|||||||
@@ -43,15 +43,13 @@ export interface Requirement {
|
|||||||
changeBy?: string;
|
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<ChangeReason, string> = {
|
export const CHANGE_REASON_LABEL: Record<ChangeReason, string> = {
|
||||||
misunderstanding: '需求理解偏差导致返工',
|
misunderstanding: '需求理解偏差导致返工',
|
||||||
feedback_rework: '反馈导致返工',
|
feedback_rework: '反馈导致返工',
|
||||||
difficulty_exceeded: '实现难度超预期',
|
difficulty_exceeded: '实现难度超预期',
|
||||||
upstream_delay: '上游交付延误',
|
upstream_delay: '上游交付延误',
|
||||||
scope_creep: '范围蔓延',
|
|
||||||
other: '其他',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SOURCE_TYPE_LABEL: Record<SourceType, string> = {
|
export const SOURCE_TYPE_LABEL: Record<SourceType, string> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user