perf(web): 优化大数据量页面切换与聚合性能
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user