perf(frontend): 递归组件 Memo 优化 — React.memo + useMemo
TaskTreeViewer: - Modal/ReceiveConfirmModal/RejectModal/TransferModal 加 React.memo - TaskNodeCard 递归组件加 React.memo,阻断父节点状态变更引发的整树重渲染 - countAllTasks 用 useMemo 缓存,仅在 task_tree 引用变化时重新计算 TaskListCard: - TaskNode 递归组件加 React.memo - countAll 用 useMemo 缓存
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo, memo } from "react";
|
||||
import {
|
||||
Search,
|
||||
Loader2,
|
||||
@ -75,7 +75,7 @@ function getStatusConfig(status: string) {
|
||||
// 通用 Modal 容器
|
||||
// ============================================================
|
||||
|
||||
function Modal({
|
||||
const Modal = memo(function Modal({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
@ -110,13 +110,13 @@ function Modal({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 确认接收弹窗
|
||||
// ============================================================
|
||||
|
||||
function ReceiveConfirmModal({
|
||||
const ReceiveConfirmModal = memo(function ReceiveConfirmModal({
|
||||
open,
|
||||
task,
|
||||
submitting,
|
||||
@ -163,13 +163,13 @@ function ReceiveConfirmModal({
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 品质驳回弹窗
|
||||
// ============================================================
|
||||
|
||||
function RejectModal({
|
||||
const RejectModal = memo(function RejectModal({
|
||||
open,
|
||||
task,
|
||||
submitting,
|
||||
@ -244,13 +244,13 @@ function RejectModal({
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 完工裂变转交弹窗
|
||||
// ============================================================
|
||||
|
||||
function TransferModal({
|
||||
const TransferModal = memo(function TransferModal({
|
||||
open,
|
||||
task,
|
||||
submitting,
|
||||
@ -496,7 +496,7 @@ function TransferModal({
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 单个任务节点卡片
|
||||
@ -507,7 +507,7 @@ interface ModalTarget {
|
||||
action: "receive" | "reject" | "transfer";
|
||||
}
|
||||
|
||||
function TaskNodeCard({
|
||||
const TaskNodeCard = memo(function TaskNodeCard({
|
||||
task,
|
||||
isLast,
|
||||
onAction,
|
||||
@ -653,7 +653,7 @@ function TaskNodeCard({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 主容器组件
|
||||
@ -779,6 +779,12 @@ export default function TaskTreeViewer() {
|
||||
|
||||
// ---- 渲染 ----
|
||||
|
||||
// 🚀 缓存递归计算 — 仅在 task_tree 变化时重新计算
|
||||
const totalTaskCount = useMemo(
|
||||
() => (product?.task_tree ? countAllTasks(product.task_tree) : 0),
|
||||
[product?.task_tree],
|
||||
);
|
||||
|
||||
const modalTask = modalTarget?.task ?? null;
|
||||
|
||||
return (
|
||||
@ -880,7 +886,7 @@ export default function TaskTreeViewer() {
|
||||
<p className="text-sm font-semibold text-gray-800">
|
||||
{product.top_level_tasks?.length ?? 0} 顶层
|
||||
{product.task_tree?.length
|
||||
? ` · ${countAllTasks(product.task_tree)} 总计`
|
||||
? ` · ${totalTaskCount} 总计`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/** 任务进度列表卡片 — 递归渲染 task_tree */
|
||||
import { useMemo, memo } from "react";
|
||||
import { ClipboardList, GitBranch, AlertTriangle } from "lucide-react";
|
||||
import type { TaskResponse, TaskSummary } from "../../types/api";
|
||||
import { TASK_STATUS } from "../../types/api";
|
||||
@ -30,7 +31,7 @@ interface TaskNodeProps {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
function TaskNode({ task, depth }: TaskNodeProps) {
|
||||
const TaskNode = memo(function TaskNode({ task, depth }: TaskNodeProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-t border-gray-50" style={{ paddingLeft: 16 + depth * 16 }}>
|
||||
@ -62,19 +63,22 @@ function TaskNode({ task, depth }: TaskNodeProps) {
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function countAll(tasks: TaskResponse[]): number {
|
||||
return tasks.reduce((s, t) => s + 1 + (t.child_tasks ? countAll(t.child_tasks) : 0), 0);
|
||||
}
|
||||
|
||||
export default function TaskListCard({ tasks }: TaskListCardProps) {
|
||||
// 🚀 缓存递归计算
|
||||
const totalCount = useMemo(() => countAll(tasks), [tasks]);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-white shadow-sm">
|
||||
<div className="flex items-center gap-2 border-b border-gray-100 px-4 py-3">
|
||||
<ClipboardList className="h-5 w-5 text-blue-600" />
|
||||
<h3 className="font-semibold text-gray-800">任务流转树</h3>
|
||||
<span className="ml-auto text-xs text-gray-400">{countAll(tasks)} 个任务</span>
|
||||
<span className="ml-auto text-xs text-gray-400">{totalCount} 个任务</span>
|
||||
</div>
|
||||
{tasks.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center text-sm text-gray-400">暂无关联任务</div>
|
||||
|
||||
Reference in New Issue
Block a user