perf(web): 优化大数据量页面切换与聚合性能

This commit is contained in:
Script Generator
2026-07-03 09:43:36 +08:00
parent 554ab520d1
commit a509bb4922
32 changed files with 1087 additions and 360 deletions

View File

@@ -15,11 +15,13 @@ import { useMemberStore } from '@/stores/useMemberStore';
import { getProjectDetail, VersionWithContext } from '@/lib/derive';
import { Stage, Role, STAGES, ROLES, STAGE_INDEX, ROLE_LABEL } from '@/lib/stage';
import { VersionStatus, VERSION_STATUS_LABEL, VERSION_STATUS_BG } from '@/lib/version-status';
import { STATUS_PROGRESS, calcGroupProgress as calcDevTaskProgress, getEstimateHours, aggregateDevTaskHours } from '@/lib/dev-task';
import { calcGroupProgress as calcDevTaskProgress, aggregateDevTaskHours } from '@/lib/dev-task';
import { CapsuleStages } from '@/components/version/CapsuleStages';
import { MemberChips } from '@/components/version/MemberChips';
import { getRequirementCoverageSummary, type VersionPlan } from '@/lib/version-plan';
import { buildVersionTimelineSummary, calcStageEffortMetrics, formatVersionOverviewDateTime, getVersionCardDefaultExpanded, mergeStageProgressWithEffort } from '@/lib/version-overview';
import { calcScopedVersionProgress } from '@/lib/version-progress';
import { buildVersionDataScopeMap, type VersionDataScope } from '@/lib/version-data-scope';
import { formatActualDuration } from '@/lib/work-hours';
import type { DevTask } from '@/lib/dev-task';
import type { TestCase } from '@/lib/test-case';
@@ -73,14 +75,10 @@ function ProgressBar({ role, percent, daysSpent }: { role: Role; percent: number
}
/* ─── VersionCard ─── */
function VersionCard({ version, progress, plans, devTasks, testCases, bugs, requirements, onNavigate }: {
function VersionCard({ version, progress, scope, onNavigate }: {
version: VersionWithContext;
progress: number;
plans: VersionPlan[];
devTasks: DevTask[];
testCases: TestCase[];
bugs: Bug[];
requirements: { id: string; versionId?: string }[];
scope: VersionDataScope;
onNavigate: (id: string) => void;
}) {
const [expanded, setExpanded] = useState(() => getVersionCardDefaultExpanded(version.status));
@@ -89,15 +87,12 @@ function VersionCard({ version, progress, plans, devTasks, testCases, bugs, requ
setExpanded(getVersionCardDefaultExpanded(version.status));
}, [version.status]);
const versionData = useMemo(() => {
const vPlans = plans.filter((p) => p.versionId === version.id);
const vReqIds = new Set(requirements.filter((r) => r.versionId === version.id).map((r) => r.id));
const vDevTasks = devTasks.filter((t) => vReqIds.has(t.requirementId));
const vTCs = testCases.filter((c) => c.versionId === version.id);
const vBugs = bugs.filter((b) => b.versionId === version.id);
return { vPlans, vDevTasks, vTCs, vBugs };
}, [version.id, plans, devTasks, testCases, bugs, requirements]);
const versionData = useMemo(() => ({
vPlans: scope.plans,
vDevTasks: scope.devTasks,
vTCs: scope.testCases,
vBugs: scope.bugs,
}), [scope]);
const stageEffortMetrics = useMemo(() => calcStageEffortMetrics({
plans: versionData.vPlans,
@@ -373,85 +368,39 @@ export default function ProjectDetailPage() {
}, [project, statusFilter, isSuperAdmin, currentUserName]);
// Compute actual overall progress per version
const versionProgressMap = useMemo(() => {
if (!project) return {} as Record<string, number>;
const map: Record<string, number> = {};
for (const v of project.versions) {
const vPlans = plans.filter((p) => p.versionId === v.id);
const vReqs = requirements.filter((r) => r.versionId === v.id);
const vReqIds = new Set(vReqs.map((r) => r.id));
const vDevTasks = devTasks.filter((t) => vReqIds.has(t.requirementId));
const vTestCases = testCases.filter((c) => c.versionId === v.id);
const versionScopeMap = useMemo(
() => project ? buildVersionDataScopeMap({
versionIds: project.versions.map((version) => version.id),
plans,
requirements,
devTasks,
testCases,
bugs,
}) : {},
[project, plans, requirements, devTasks, testCases, bugs],
);
const segments: number[] = [];
const researchPlans = vPlans.filter((p) => p.type === 'research');
if (researchPlans.length > 0) {
const totals = researchPlans.reduce((acc, p) => {
const tasks = p.tasks || [];
acc.total += tasks.length;
acc.done += tasks.filter((t) => t.status === 'completed').length;
return acc;
}, { total: 0, done: 0 });
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
}
const productPlans = vPlans.filter((p) => p.type === 'product');
if (productPlans.length > 0) {
const totals = productPlans.reduce((acc, p) => {
const summary = getRequirementCoverageSummary(p);
acc.total += summary.total;
acc.done += summary.completed;
return acc;
}, { total: 0, done: 0 });
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
}
const uiPlans = vPlans.filter((p) => p.type === 'ui');
if (uiPlans.length > 0) {
const totals = uiPlans.reduce((acc, p) => {
const summary = getRequirementCoverageSummary(p);
acc.total += summary.total;
acc.done += summary.completed;
return acc;
}, { total: 0, done: 0 });
segments.push(totals.total > 0 ? (totals.done / totals.total) * 100 : 0);
}
if (vDevTasks.length > 0) {
const totalEstimate = vDevTasks.reduce((sum, t) => sum + getEstimateHours(t), 0);
let devProgress: number;
if (totalEstimate === 0) {
devProgress = vDevTasks.reduce((sum, t) => sum + STATUS_PROGRESS[t.status], 0) / vDevTasks.length;
} else {
const weighted = vDevTasks.reduce((sum, t) => sum + getEstimateHours(t) * STATUS_PROGRESS[t.status], 0);
devProgress = weighted / totalEstimate;
}
segments.push(devProgress);
}
if (vTestCases.length > 0) {
const executed = vTestCases.filter((c) => c.status === 'passed' || c.status === 'failed' || c.status === 'blocked').length;
segments.push((executed / vTestCases.length) * 100);
}
map[v.id] = segments.length > 0 ? Math.round(segments.reduce((s, x) => s + x, 0) / segments.length) : 0;
}
return map;
}, [project, plans, requirements, devTasks, testCases]);
const versionProgressMap = useMemo(
() => Object.fromEntries(
Object.entries(versionScopeMap).map(([id, scope]) => [
id,
calcScopedVersionProgress(scope.plans, scope.devTasks, scope.testCases),
]),
),
[versionScopeMap],
);
const stats = useMemo(() => {
if (!project) return { total: 0, released: 0, reqCount: 0, bugCount: 0, estimateHours: 0, actualHours: 0 };
const total = project.versions.length;
const released = project.versions.filter((v) => v.status === 'released').length;
const reqCount = requirements.filter((r) => r.projectId === projectId).length;
const versionIds = new Set(project.versions.map((v) => v.id));
const bugCount = bugs.filter((b) => versionIds.has(b.versionId)).length;
const projectReqIds = new Set(requirements.filter((r) => r.projectId === projectId).map((r) => r.id));
const projectDevTasks = devTasks.filter((t) => projectReqIds.has(t.requirementId));
const versionScopes = Object.values(versionScopeMap);
const bugCount = versionScopes.reduce((sum, scope) => sum + scope.bugs.length, 0);
const projectDevTasks = versionScopes.flatMap((scope) => scope.devTasks);
const { estimate, actual } = aggregateDevTaskHours(projectDevTasks);
return { total, released, reqCount, bugCount, estimateHours: estimate, actualHours: actual };
}, [project, requirements, bugs, devTasks, projectId]);
}, [project, requirements, projectId, versionScopeMap]);
const teamByRole = useMemo(() => {
if (!project) return {} as Record<string, Record<string, number>>;
@@ -524,7 +473,7 @@ export default function ProjectDetailPage() {
{sortedVersions.length === 0 ? (
<div className="rounded-xl border border-dashed border-[var(--line)] p-6 text-center text-xs text-[var(--ink-muted)]"></div>
) : (
sortedVersions.map((v) => <VersionCard key={v.id} version={v} progress={versionProgressMap[v.id] ?? 0} plans={plans} devTasks={devTasks} testCases={testCases} bugs={bugs} requirements={requirements} onNavigate={(id) => router.push(`/versions/${id}`)} />)
sortedVersions.map((v) => <VersionCard key={v.id} version={v} progress={versionProgressMap[v.id] ?? 0} scope={versionScopeMap[v.id]} onNavigate={(id) => router.push(`/versions/${id}`)} />)
)}
</div>
</section>