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

@@ -34,7 +34,8 @@ import { calcBugSeverityRanking, calcPersonalEffortRanking, calcStageEffortMetri
import { addVersionMembers, DEFAULT_VERSION_MEMBER_ROLE, filterVersionMemberCandidates } from '@/lib/version-members';
import { addRecommendedVersionMembers, getDefaultRecommendedMemberNames, recommendVersionMembers, type MemberRecommendationGroup, type RecommendableRole } from '@/lib/member-recommendation';
import { getRequirementCoverageSummary } from '@/lib/version-plan';
import { buildVersionProgressMap } from '@/lib/version-progress';
import { buildVersionDataScope } from '@/lib/version-data-scope';
import { calcScopedVersionProgress } from '@/lib/version-progress';
import { canSubmitReleaseForm, getReleaseProgressWarning } from '@/lib/version-release';
import { isVersionReadonly } from '@/lib/version-status';
import {
@@ -139,21 +140,33 @@ export default function VersionDetailPage() {
}, [fetchMembers, fetchCategories]);
const version = useMemo(() => getVersionDetail(overview, versionId), [overview, versionId]);
const releaseProgressMap = useMemo<Record<string, number>>(
() => version ? buildVersionProgressMap([version], plans, requirements, devTasks, testCases) : {},
[version, plans, requirements, devTasks, testCases],
const versionScope = useMemo(
() => version
? buildVersionDataScope({
versionId: version.id,
requirements,
plans,
devTasks,
testCases,
bugs,
overtimeRecords: records,
})
: null,
[version, requirements, plans, devTasks, testCases, bugs, records],
);
const releaseProgress = version ? (releaseProgressMap[version.id] ?? 0) : 0;
const releaseProgress = versionScope
? calcScopedVersionProgress(versionScope.plans, versionScope.devTasks, versionScope.testCases)
: 0;
const versionReadonly = version ? isVersionReadonly(version.status) : false;
// 自动同步版本状态:有计划开始时间<=今天,版本应进入对应阶段
useEffect(() => {
if (!version || !plans.length) return;
if (!version || !versionScope || !versionScope.plans.length) return;
if (version.status !== 'planned' && version.status !== 'developing') return;
const today = new Date().toISOString().slice(0, 10);
const stageMap = { research: 'requirement', product: 'product_design', ui: 'ui_design' } as const;
const stageOrder: string[] = ['requirement', 'product_design', 'ui_design'];
const versionPlans = plans.filter((p) => p.versionId === versionId && p.startTime <= today);
const versionPlans = versionScope.plans.filter((p) => p.startTime <= today);
if (versionPlans.length === 0) return;
let targetStage = '';
for (const p of versionPlans) {
@@ -165,7 +178,7 @@ export default function VersionDetailPage() {
if (version.status === 'planned' || (version.currentStage && stageOrder.indexOf(targetStage) > stageOrder.indexOf(version.currentStage))) {
updateVersion(version.productId, version.id, { status: 'developing', currentStage: targetStage as any });
}
}, [plans, version, versionId, updateVersion]);
}, [versionScope, version, updateVersion]);
if (!version) {
return (
@@ -189,36 +202,36 @@ export default function VersionDetailPage() {
);
}
const recommendationVersionReqs = requirements.filter((requirement) => requirement.versionId === version.id);
const recommendationVersionReqIds = new Set(recommendationVersionReqs.map((requirement) => requirement.id));
const recommendationVersionDevTasks = devTasks.filter((task) => recommendationVersionReqIds.has(task.requirementId));
const recommendationVersionTestCases = testCases.filter((testCase) => testCase.versionId === version.id);
const currentSystemParticipation = new Map<string, number>();
overview.forEach((product) => {
product.versions.forEach((item) => {
const uniqueNames = new Set((item.members ?? []).map((member) => member.name));
uniqueNames.forEach((name) => currentSystemParticipation.set(name, (currentSystemParticipation.get(name) ?? 0) + 1));
const scopedVersionData = versionScope!;
const memberRecommendationGroups = showRecommendModal && recommendationDataReady ? (() => {
const currentSystemParticipation = new Map<string, number>();
overview.forEach((product) => {
product.versions.forEach((item) => {
const uniqueNames = new Set((item.members ?? []).map((member) => member.name));
uniqueNames.forEach((name) => currentSystemParticipation.set(name, (currentSystemParticipation.get(name) ?? 0) + 1));
});
});
});
const memberRecommendationGroups = recommendVersionMembers({
candidates: memberCandidates,
currentMembers: version.members ?? [],
devTasks,
testCases,
bugs,
overtimeRecords: records,
versionDeadline: version.expectedReleaseDate,
scopeRequirements: recommendationVersionReqs,
scopeDevTasks: recommendationVersionDevTasks,
scopeTestCases: recommendationVersionTestCases,
taskCategories,
historicalStats: memberCandidates.flatMap((member) => {
const projectParticipationCount = currentSystemParticipation.get(member.name);
return typeof projectParticipationCount === 'number'
? [{ name: member.name, projectParticipationCount }]
: [];
}),
});
return recommendVersionMembers({
candidates: memberCandidates,
currentMembers: version.members ?? [],
devTasks,
testCases,
bugs,
overtimeRecords: records,
versionDeadline: version.expectedReleaseDate,
scopeRequirements: scopedVersionData.requirements,
scopeDevTasks: scopedVersionData.devTasks,
scopeTestCases: scopedVersionData.testCases,
taskCategories,
historicalStats: memberCandidates.flatMap((member) => {
const projectParticipationCount = currentSystemParticipation.get(member.name);
return typeof projectParticipationCount === 'number'
? [{ name: member.name, projectParticipationCount }]
: [];
}),
});
})() : [];
const renderActions = () => {
const buttons: { label: string; action: () => void; danger?: boolean; tone?: 'release'; icon?: JSX.Element }[] = [];
@@ -231,15 +244,14 @@ export default function VersionDetailPage() {
buttons.push({ label: '删除', action: () => {
if (confirm('确认删除该版本关联的需求会回到需求池版本下的计划、开发任务、测试用例、Bug 将被清除。')) {
// 释放关联需求
requirements.filter((r) => r.versionId === version.id).forEach((r) => updateRequirement(r.id, getRequirementVersionUnlinkPatch(r)));
scopedVersionData.requirements.forEach((r) => updateRequirement(r.id, getRequirementVersionUnlinkPatch(r)));
// 清理计划任务
plans.filter((p) => p.versionId === version.id).forEach((p) => deletePlan(p.id));
scopedVersionData.plans.forEach((p) => deletePlan(p.id));
// 清理开发任务
const versionReqIds = new Set(requirements.filter((r) => r.versionId === version.id).map((r) => r.id));
devTasks.filter((t) => versionReqIds.has(t.requirementId)).forEach((t) => deleteDevTask(t.id));
scopedVersionData.devTasks.forEach((t) => deleteDevTask(t.id));
// 清理测试用例和Bug
testCases.filter((c) => c.versionId === version.id).forEach((c) => deleteTestCase(c.id));
bugs.filter((b) => b.versionId === version.id).forEach((b) => deleteBug(b.id));
scopedVersionData.testCases.forEach((c) => deleteTestCase(c.id));
scopedVersionData.bugs.forEach((b) => deleteBug(b.id));
deleteVersion(version.productId, version.id);
router.push('/versions');
}
@@ -304,13 +316,12 @@ export default function VersionDetailPage() {
{activeTab === 'overview' ? (
(() => {
const now = new Date();
const versionReqs = requirements.filter((r) => r.versionId === version.id);
const versionReqIds = new Set(versionReqs.map((r) => r.id));
const versionPlans = plans.filter((p) => p.versionId === version.id);
const versionDevTasks = devTasks.filter((t) => versionReqIds.has(t.requirementId));
const versionTCs = testCases.filter((c) => c.versionId === version.id);
const versionBugs = bugs.filter((b) => b.versionId === version.id);
const versionOT = records.filter((r) => r.versionId === version.id);
const versionReqs = scopedVersionData.requirements;
const versionPlans = scopedVersionData.plans;
const versionDevTasks = scopedVersionData.devTasks;
const versionTCs = scopedVersionData.testCases;
const versionBugs = scopedVersionData.bugs;
const versionOT = scopedVersionData.overtimeRecords;
const stageEffortMetrics = calcStageEffortMetrics({
plans: versionPlans,
devTasks: versionDevTasks,
@@ -663,7 +674,7 @@ export default function VersionDetailPage() {
{/* 6. 需求变更统计 */}
{(() => {
const changeReqs = requirements.filter((r) => r.versionId === version.id && r.reqType === 'change');
const changeReqs = versionReqs.filter((r) => r.reqType === 'change');
if (changeReqs.length === 0) return null;
// 变更人员排名
@@ -900,7 +911,7 @@ export default function VersionDetailPage() {
const projectAdoptedReqs = getProjectAdoptedRequirementCandidates(requirements, version.projectId);
return (
<PlanTab
plans={plans}
plans={scopedVersionData.plans}
versionId={version.id}
version={version}
versionDeadline={version.expectedReleaseDate ?? undefined}
@@ -942,27 +953,32 @@ export default function VersionDetailPage() {
);
})()
) : activeTab === 'tasks' ? (
(() => {
const versionReqs = requirements.filter((r) => r.versionId === version.id);
return (
<DevTaskTab
versionId={version.id}
requirementIds={versionReqs.map((r) => r.id)}
versionDeadline={version.expectedReleaseDate ?? undefined}
readOnly={versionReadonly}
/>
);
})()
<DevTaskTab
versionId={version.id}
requirementIds={scopedVersionData.requirementIds}
versionDeadline={version.expectedReleaseDate ?? undefined}
readOnly={versionReadonly}
versionTasks={scopedVersionData.devTasks}
versionRequirements={scopedVersionData.requirements}
/>
) : activeTab === 'testcases' ? (
(() => {
const versionReqs = requirements.filter((r) => r.versionId === version.id);
return <TestCaseTab versionId={version.id} requirementIds={versionReqs.map((r) => r.id)} readOnly={versionReadonly} />;
})()
<TestCaseTab
versionId={version.id}
requirementIds={scopedVersionData.requirementIds}
readOnly={versionReadonly}
versionCases={scopedVersionData.testCases}
versionBugs={scopedVersionData.bugs}
versionDevTasks={scopedVersionData.devTasks}
versionRequirements={scopedVersionData.requirements}
/>
) : activeTab === 'bugs' ? (
(() => {
const versionReqs = requirements.filter((r) => r.versionId === version.id);
return <BugTab versionId={version.id} requirementIds={versionReqs.map((r) => r.id)} readOnly={versionReadonly} />;
})()
<BugTab
versionId={version.id}
requirementIds={scopedVersionData.requirementIds}
readOnly={versionReadonly}
versionBugs={scopedVersionData.bugs}
versionTestCases={scopedVersionData.testCases}
/>
) : (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-12 flex items-center justify-center">
<span className="text-[13px] text-[var(--ink-muted)]"></span>