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

@@ -10,11 +10,15 @@ import { useAuthStore } from '@/stores/useAuthStore';
import { flattenProjects, flattenVersions } from '@/lib/derive';
import { REQ_STATUS_LABEL, REQ_STATUS_COLOR, SOURCE_TYPE_LABEL } from '@/lib/requirement';
import type { Requirement, RequirementStatus, SourceType } from '@/lib/requirement';
import { deriveReqDevStatus, canEditRequirement, canCloseRequirement, REQ_DEV_STATUS_LABEL, REQ_DEV_STATUS_COLOR } from '@/lib/linkage-engine';
import { buildRequirementDevStatusMap, canEditRequirement, canCloseRequirement, REQ_DEV_STATUS_LABEL, REQ_DEV_STATUS_COLOR } from '@/lib/linkage-engine';
import { buildRequirementScopeTree, filterRequirementScopeTreeByKeyword, filterRequirementsByScope, type RequirementProductScopeNode, type RequirementScopeSelection } from '@/lib/requirement-scope';
import { sortRequirementsByCreatedAt, type RequirementDateSort } from '@/lib/requirement-sort';
import {
REQUIREMENT_TABLE_BADGE_CLASS,
REQUIREMENT_TABLE_CLASS,
REQUIREMENT_TABLE_COLUMN_WIDTHS,
REQUIREMENT_TABLE_CONTAINER_CLASS,
REQUIREMENT_TABLE_HEADER_CELL_CLASS,
REQUIREMENT_TABLE_NOWRAP_CELL_CLASS,
REQUIREMENT_TABLE_SMALL_BADGE_CLASS,
getRequirementTableTextClass,
@@ -178,6 +182,17 @@ function RequirementsPageContent() {
const currentUserName = user?.name || '系统';
const allProjects = useMemo(() => flattenProjects(overview), [overview]);
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
const requirementDevStatusMap = useMemo(() => buildRequirementDevStatusMap(devTasks), [devTasks]);
const devTasksByRequirement = useMemo(() => {
const map = new Map<string, typeof devTasks>();
for (const task of devTasks) {
if (!task.requirementId) continue;
const items = map.get(task.requirementId) ?? [];
items.push(task);
map.set(task.requirementId, items);
}
return map;
}, [devTasks]);
const [selectedScope, setSelectedScope] = useState<RequirementScopeSelection>({ type: 'all' });
const [scopeSearch, setScopeSearch] = useState('');
@@ -261,7 +276,7 @@ function RequirementsPageContent() {
// status filter
if (statusFilter !== 'all') {
list = statusFilter === 'dev_completed'
? list.filter((r) => deriveReqDevStatus(r.id, devTasks) === 'completed')
? list.filter((r) => requirementDevStatusMap.get(r.id) === 'completed')
: list.filter((r) => r.status === statusFilter);
}
@@ -281,7 +296,7 @@ function RequirementsPageContent() {
list = sortRequirementsByCreatedAt(list, dateSort);
return list;
}, [scopedRequirements, search, statusFilter, priorityFilter, typeFilter, versionFilter, dateSort, devTasks]);
}, [scopedRequirements, search, statusFilter, priorityFilter, typeFilter, versionFilter, dateSort, requirementDevStatusMap]);
const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filtered, 20);
@@ -450,22 +465,27 @@ function RequirementsPageContent() {
</div>
) : (
<>
<div className="overflow-x-auto rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] shadow-[var(--shadow-sm)]">
<table className="min-w-[1120px] w-full text-left text-[13px]">
<div className={REQUIREMENT_TABLE_CONTAINER_CLASS}>
<table className={REQUIREMENT_TABLE_CLASS}>
<colgroup>
{REQUIREMENT_TABLE_COLUMN_WIDTHS.map((width, index) => (
<col key={index} style={{ width: `${width}%` }} />
))}
</colgroup>
<thead className="sticky top-0 z-10 bg-[var(--bg-subtle)]">
<tr className="border-b border-[var(--line)] bg-[var(--bg-subtle)]">
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th className={REQUIREMENT_TABLE_HEADER_CELL_CLASS}></th>
<th
className="px-4 py-2.5 text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)] cursor-pointer select-none hover:text-[var(--ink-soft)] transition-colors"
className={`${REQUIREMENT_TABLE_HEADER_CELL_CLASS} cursor-pointer select-none transition-colors hover:text-[var(--ink-soft)]`}
onClick={() => setDateSort(dateSort === 'desc' ? 'asc' : 'desc')}
>
<span className="inline-flex items-center gap-1">
@@ -473,7 +493,7 @@ function RequirementsPageContent() {
{dateSort === 'desc' ? <ArrowDown className="h-3 w-3" /> : <ArrowUp className="h-3 w-3" />}
</span>
</th>
<th className="px-4 py-2.5 text-right text-[11px] font-medium uppercase tracking-wide text-[var(--ink-muted)]"></th>
<th className={`${REQUIREMENT_TABLE_HEADER_CELL_CLASS} text-right`}></th>
</tr>
</thead>
<tbody>
@@ -545,13 +565,20 @@ function RequirementsPageContent() {
{/* 所属版本 */}
<td className="px-4 py-3 text-[12px] text-[var(--ink-soft)]">
{resolveVersionName(req.versionId)}
{(() => {
const versionName = resolveVersionName(req.versionId);
return (
<span className={getRequirementTableTextClass('version')} title={versionName}>
{versionName}
</span>
);
})()}
</td>
{/* 开发状态 */}
<td className={REQUIREMENT_TABLE_NOWRAP_CELL_CLASS}>
{(() => {
const devStatus = deriveReqDevStatus(req.id, devTasks);
const devStatus = requirementDevStatusMap.get(req.id) ?? 'unscheduled';
return (
<span className={`${REQUIREMENT_TABLE_SMALL_BADGE_CLASS} ${REQ_DEV_STATUS_COLOR[devStatus]}`}>
{REQ_DEV_STATUS_LABEL[devStatus]}
@@ -573,13 +600,13 @@ function RequirementsPageContent() {
</td>
{/* 操作 */}
<td className="px-4 py-3 text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-1 whitespace-nowrap">
<td className="px-3 py-3 text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end gap-0.5 whitespace-nowrap">
{/* 编辑:待评审/已采纳/已规划,且开发中不可编辑 */}
{(['pending_review', 'adopted', 'planned'] as const).includes(req.status as any) && canEditRequirement(req.id, devTasks) && (
{(['pending_review', 'adopted', 'planned'] as const).includes(req.status as any) && canEditRequirement(req.id, devTasksByRequirement.get(req.id) ?? []) && (
<button
onClick={() => handleEdit(req)}
className="h-6 px-2 rounded text-[11px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] transition-colors"
className="h-6 rounded px-1.5 text-[11px] font-medium text-[var(--ink-soft)] transition-colors hover:bg-[var(--bg-subtle)]"
>
</button>
@@ -589,23 +616,23 @@ function RequirementsPageContent() {
<>
<button
onClick={() => updateRequirement(req.id, { status: 'adopted' })}
className="h-6 px-2 rounded text-[11px] font-medium text-emerald-600 hover:bg-emerald-50 transition-colors"
className="h-6 rounded px-1.5 text-[11px] font-medium text-emerald-600 transition-colors hover:bg-emerald-50"
>
</button>
<button
onClick={() => { setRejectingReq(req); setRejectReason(''); }}
className="h-6 px-2 rounded text-[11px] font-medium text-red-600 hover:bg-red-50 transition-colors"
className="h-6 rounded px-1.5 text-[11px] font-medium text-red-600 transition-colors hover:bg-red-50"
>
</button>
</>
)}
{/* 已采纳/已规划:关闭 */}
{(req.status === 'adopted' || req.status === 'planned') && canCloseRequirement(req.id, devTasks) && (
{(req.status === 'adopted' || req.status === 'planned') && canCloseRequirement(req.id, devTasksByRequirement.get(req.id) ?? []) && (
<button
onClick={() => updateRequirement(req.id, { status: 'closed' })}
className="h-6 px-2 rounded text-[11px] font-medium text-orange-600 hover:bg-orange-50 transition-colors"
className="h-6 rounded px-1.5 text-[11px] font-medium text-orange-600 transition-colors hover:bg-orange-50"
>
</button>
@@ -614,7 +641,7 @@ function RequirementsPageContent() {
{(['pending_review', 'rejected', 'closed'] as const).includes(req.status as any) && (
<button
onClick={() => deleteRequirement(req.id)}
className="h-6 px-2 rounded text-[11px] font-medium text-red-500 hover:bg-red-50 transition-colors"
className="h-6 rounded px-1.5 text-[11px] font-medium text-red-500 transition-colors hover:bg-red-50"
>
</button>
@@ -644,10 +671,10 @@ function RequirementsPageContent() {
requirements={requirements}
resolveVersionName={resolveVersionName}
onClose={() => setViewingReq(null)}
onEdit={canEditRequirement(viewingReq.id, devTasks) ? () => handleEdit(viewingReq) : undefined}
onEdit={canEditRequirement(viewingReq.id, devTasksByRequirement.get(viewingReq.id) ?? []) ? () => handleEdit(viewingReq) : undefined}
onAdopt={() => { updateRequirement(viewingReq.id, { status: 'adopted' }); setViewingReq(null); }}
onReject={() => { setRejectingReq(viewingReq); setRejectReason(''); setViewingReq(null); }}
onCloseReq={canCloseRequirement(viewingReq.id, devTasks) ? () => { updateRequirement(viewingReq.id, { status: 'closed' }); setViewingReq(null); } : undefined}
onCloseReq={canCloseRequirement(viewingReq.id, devTasksByRequirement.get(viewingReq.id) ?? []) ? () => { updateRequirement(viewingReq.id, { status: 'closed' }); setViewingReq(null); } : undefined}
onDelete={() => { deleteRequirement(viewingReq.id); setViewingReq(null); }}
/>
)}