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