diff --git a/frontend/src/components/TaskTree/TaskTreeViewer.tsx b/frontend/src/components/TaskTree/TaskTreeViewer.tsx index f0cba09..bb9dea3 100644 --- a/frontend/src/components/TaskTree/TaskTreeViewer.tsx +++ b/frontend/src/components/TaskTree/TaskTreeViewer.tsx @@ -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({ ); -} +}); // ============================================================ // 确认接收弹窗 // ============================================================ -function ReceiveConfirmModal({ +const ReceiveConfirmModal = memo(function ReceiveConfirmModal({ open, task, submitting, @@ -163,13 +163,13 @@ function ReceiveConfirmModal({ ); -} +}); // ============================================================ // 品质驳回弹窗 // ============================================================ -function RejectModal({ +const RejectModal = memo(function RejectModal({ open, task, submitting, @@ -244,13 +244,13 @@ function RejectModal({ ); -} +}); // ============================================================ // 完工裂变转交弹窗 // ============================================================ -function TransferModal({ +const TransferModal = memo(function TransferModal({ open, task, submitting, @@ -496,7 +496,7 @@ function TransferModal({ ); -} +}); // ============================================================ // 单个任务节点卡片 @@ -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({ )} ); -} +}); // ============================================================ // 主容器组件 @@ -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() {

{product.top_level_tasks?.length ?? 0} 顶层 {product.task_tree?.length - ? ` · ${countAllTasks(product.task_tree)} 总计` + ? ` · ${totalTaskCount} 总计` : ""}

diff --git a/frontend/src/components/scan/TaskListCard.tsx b/frontend/src/components/scan/TaskListCard.tsx index 84d5dec..c123d4e 100644 --- a/frontend/src/components/scan/TaskListCard.tsx +++ b/frontend/src/components/scan/TaskListCard.tsx @@ -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 ( <>
@@ -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 (

任务流转树

- {countAll(tasks)} 个任务 + {totalCount} 个任务
{tasks.length === 0 ? (
暂无关联任务