From 974f4b9d01abe04f85818b693d944f5ea232fe4b Mon Sep 17 00:00:00 2001 From: duxingchen Date: Tue, 11 Aug 2026 15:37:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20TaskFlowView=20=E6=A0=B9?= =?UTF-8?q?=E6=B2=BB=E5=8F=8C=E9=87=8D=E6=B8=B2=E6=9F=93=20=E2=80=94=20all?= =?UTF-8?q?Mains=20=E4=BB=85=E5=90=AB=E6=A0=B9=E7=BA=A7=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生产数据问题:16个任务中15个TRANSFER子节点全部挂在同一个root下, isMain() 将它们同时放入 allMains 和 childMap,导致每个子节点渲染2次。 修复: 1. allMains 改为仅收集 parent_task_id IS NULL 的根级主干 - TRANSFER 子节点仅通过 childMap → renderBranch 递归渲染 - 彻底杜绝同一任务在时间线 + 分支中双重出现 2. 统一焦点/全景为同一套垂直时间线 JSX 布局 - 删除旧焦点模式的独立 flex-row 排版 - visibleMains useMemo 负责数据过滤 (active() 过滤) 3. 清理死代码: getRootMainId() / rootMainMap / srm / flatMap --- .../src/components/TaskTree/TaskFlowView.tsx | 148 ++++++------------ 1 file changed, 49 insertions(+), 99 deletions(-) diff --git a/frontend/src/components/TaskTree/TaskFlowView.tsx b/frontend/src/components/TaskTree/TaskFlowView.tsx index 4447d40..2dd6834 100644 --- a/frontend/src/components/TaskTree/TaskFlowView.tsx +++ b/frontend/src/components/TaskTree/TaskFlowView.tsx @@ -13,16 +13,6 @@ import type { ModalTarget } from "./TaskTreeViewer"; // ============================================================ function fmtTime(d: string | null) { if (!d) return ""; const dt = new Date(d); const pad = (n: number) => String(n).padStart(2, "0"); return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`; } function isMain(t: TaskResponse) { return !t.parent_task_id || t.task_type === "TRANSFER" || t.task_type === "RECOVERY"; } -/** 寻根算法:从任意任务向上攀爬,找到最近的视觉主干节点 ID */ -function getRootMainId(t: TaskResponse, flatMap: Record): string { - let curr: TaskResponse = t; - while (curr.parent_task_id && flatMap[curr.parent_task_id]) { - const p = flatMap[curr.parent_task_id]; - if (isMain(p)) return p.id; - curr = p; - } - return curr.parent_task_id || curr.id; -} function active(s: string) { return s === "WIP" || s === "PENDING"; } /** 微型右箭头 SVG */ function ArrowRight({ color = "#9ca3af" }: { color?: string }) { @@ -108,34 +98,16 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren const [showFullMap, setShowFullMap] = useState(false); const [recordsTask, setRecordsTask] = useState(null); - // 数据分类 — 🚀 虚拟扁平化:三层+嵌套协助统一挂在视觉主干下 - const { activeMain, historicalMain, childMap, rootMainMap } = useMemo(() => { + // 数据分类 — 🚀 仅根级主干作为垂直时间线节点,子节点通过 childMap 分支递归渲染 + const { allMains, childMap } = useMemo(() => { ALL_TASKS.clear(); if (tasks.length) collectAll(tasks); const all = Array.from(ALL_TASKS); - // 拍平映射 - const flatMap: Record = {}; - for (const t of all) flatMap[t.id] = t; - // 计算每个 sub 任务的视觉根主干 ID - const rootMap: Record = {}; - const srm: Record = {}; - const am: TaskResponse[] = []; const hm: TaskResponse[] = []; + // 🔧 仅收集根级主干节点(parent_task_id IS NULL),杜绝 TRANSFER 子节点双重渲染 + const mains: TaskResponse[] = []; for (const t of all) { - if (isMain(t)) { - if (active(t.status)) am.push(t); else hm.push(t); - srm[t.id] = []; - } + if (!t.parent_task_id && isMain(t)) mains.push(t); } - // 非主线任务:按根主干分组 - for (const t of all) { - if (isMain(t)) continue; - const rootId = getRootMainId(t, flatMap); - rootMap[t.id] = rootId; - if (!srm[rootId]) srm[rootId] = []; - srm[rootId].push(t); - } - // 排序 - am.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); - hm.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); + mains.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); // 🚀 构建全局 childMap(按 parent_task_id 索引直接子节点,保留真实树结构) const childMap: Record = {}; for (const t of all) { @@ -144,11 +116,9 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren childMap[pid].push(t); } Object.values(childMap).forEach(arr => arr.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())); - return { activeMain: am, historicalMain: hm, childMap, rootMainMap: rootMap }; + return { allMains: mains, childMap }; }, [tasks]); - const allMains = [...historicalMain, ...activeMain]; - // 🚀 递归渲染分支节点 — 每个节点从自己的 childMap 获取直系子孙,保持树结构不断裂 const renderBranch = (node: TaskResponse, side: 'left' | 'right', isLegacy: boolean, rootMainId: string): JSX.Element => { const kids = childMap[node.id] || []; @@ -172,6 +142,13 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren ); }; + // 🚀 焦点模式 vs 全景模式:共用同一套垂直时间线布局,仅数据过滤不同 + const visibleMains = useMemo(() => { + if (showFullMap) return allMains; + // 焦点模式:过滤掉已完成/已入库的历史主线,保留活跃主线 + 所有协助分支 + return allMains.filter(t => active(t.status)); + }, [allMains, showFullMap]); + return (
{/* 模式切换 */} @@ -182,78 +159,51 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
- {/* ─── 焦点模式 ─── */} - {!showFullMap && ( -
- {activeMain.map(mainTask => { - const directChildren = childMap[mainTask.id] || []; - const leftDirect = directChildren.filter((_, i) => i % 2 === 0); - const rightDirect = directChildren.filter((_, i) => i % 2 === 1); - const isLegacy = !active(mainTask.status); - return ( -
+ {/* ─── 统一垂直时间线布局(焦点/全景共用) ─── */} +
+ {visibleMains.map(mainTask => { + const directChildren = childMap[mainTask.id] || []; + const hasLegacyActive = directChildren.some(c => !isMain(c) && !active(mainTask.status)); + const leftDirect = directChildren.filter((_, i) => i % 2 === 0); + const rightDirect = directChildren.filter((_, i) => i % 2 === 1); + const lineStyle = hasLegacyActive && !active(mainTask.status); + + return ( +
+
+
{/* 左翼 — 递归渲染,子子孙孙向外延伸 */}
- {leftDirect.map(t => renderBranch(t, 'left', isLegacy, mainTask.id))} + {leftDirect.map(c => renderBranch(c, 'left', lineStyle, mainTask.id))}
- {/* 中央主干 */} -
- + {/* 中央 */} +
+ + {active(mainTask.status) && ( +
+ )}
{/* 右翼 — 递归渲染,子子孙孙向外延伸 */}
- {rightDirect.map(t => renderBranch(t, 'right', isLegacy, mainTask.id))} + {rightDirect.map(c => renderBranch(c, 'right', lineStyle, mainTask.id))}
- ); - })} - {activeMain.length === 0 &&

当前无活跃主线任务

} -
- )} - {/* ─── 全景模式 ─── */} - {showFullMap && ( -
- {allMains.map(mainTask => { - const directChildren = childMap[mainTask.id] || []; - const hasLegacyActive = directChildren.some(c => !isMain(c) && !active(mainTask.status)); - const leftDirect = directChildren.filter((_, i) => i % 2 === 0); - const rightDirect = directChildren.filter((_, i) => i % 2 === 1); - const lineStyle = hasLegacyActive && !active(mainTask.status); - - return ( -
-
-
- {/* 左翼 — 递归渲染,子子孙孙向外延伸 */} -
- {leftDirect.map(c => renderBranch(c, 'left', lineStyle, mainTask.id))} -
- {/* 中央 */} -
- - {active(mainTask.status) && ( -
- )} -
- {/* 右翼 — 递归渲染,子子孙孙向外延伸 */} -
- {rightDirect.map(c => renderBranch(c, 'right', lineStyle, mainTask.id))} -
+ {visibleMains.indexOf(mainTask) < visibleMains.length - 1 && ( +
+
- - {allMains.indexOf(mainTask) < allMains.length - 1 && ( -
- -
- )} -
- ); - })} - {allMains.length === 0 &&

暂无流转记录

} -
- )} + )} +
+ ); + })} + {visibleMains.length === 0 && ( +

+ {showFullMap ? "暂无流转记录" : "当前无活跃主线任务"} +

+ )} +
{/* 记录弹窗 */} {recordsTask && (