From 6373dca4f67bcf0482f217f7036862ecd65c688b Mon Sep 17 00:00:00 2001 From: duxingchen Date: Mon, 10 Aug 2026 17:37:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20Web=20=E7=AB=AF=E5=8D=81=E5=AD=97?= =?UTF-8?q?=E6=98=9F=E6=B5=81=E8=BD=AC=E6=A0=91=20=E2=80=94=203=E5=B1=82?= =?UTF-8?q?=E5=B5=8C=E5=A5=97=E5=8D=8F=E5=8A=A9=E9=99=8D=E7=BB=B4=E6=B8=B2?= =?UTF-8?q?=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isMain() 移除危险的 parent.status 兜底判定,仅凭基因字段 - 新增 getRootMainId() 寻根算法,向上攀爬找视觉主干 - subsByRootMain 虚拟扁平化:孙子协助挂在顶级主干下 - 层级排序:直接子节点 → 孙子按父链收集 - SlimCard 新增嵌套协助标识:协助: {真实parent.assignee_id} --- .../src/components/TaskTree/TaskFlowView.tsx | 106 ++++++++++++------ 1 file changed, 72 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/TaskTree/TaskFlowView.tsx b/frontend/src/components/TaskTree/TaskFlowView.tsx index 451acf9..95d7f9f 100644 --- a/frontend/src/components/TaskTree/TaskFlowView.tsx +++ b/frontend/src/components/TaskTree/TaskFlowView.tsx @@ -12,7 +12,17 @@ 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" || (!t.task_type && !!t.parent_task_id); } +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 }) { @@ -33,16 +43,18 @@ function findParent(child: TaskResponse): TaskResponse | undefined { for (const // 极简卡片 // ============================================================ const SlimCard = memo(function SlimCard({ - task, assigneeName, active, legacy, onAction, currentUser, onViewRecords, + task, assigneeName, active, legacy, onAction, currentUser, onViewRecords, rootMainId, }: { task: TaskResponse; assigneeName?: string; active: boolean; legacy?: boolean; onAction: (t: ModalTarget) => void; currentUser?: { username?: string; role?: string } | null; onViewRecords?: (t: TaskResponse) => void; + rootMainId?: string; }) { const cfg = getStatusConfig(task.status); const isOwner = !!(currentUser?.username && task.assignee_id === currentUser.username); const isManager = currentUser?.role === "SUPER_ADMIN" || currentUser?.role === "SUPERVISOR"; const main = isMain(task); + const isNestedSpawn = !main && rootMainId && task.parent_task_id !== rootMainId && !!task.parent_task_id; return (
@@ -58,6 +70,7 @@ const SlimCard = memo(function SlimCard({ {task.completed_at ? ` → ${fmtTime(task.completed_at).split(" ")[0]}` : " → 至今"}

{legacy &&

源自: {assigneeName || (findParent(task)?.assignee_id) || "历史任务"}

} + {isNestedSpawn &&

协助: {findParent(task)?.assignee_id || "—"}

} {/* 操作按钮 */} {active && isOwner && (
@@ -95,24 +108,58 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren const [showFullMap, setShowFullMap] = useState(false); const [recordsTask, setRecordsTask] = useState(null); - // 数据分类 - const { activeMain, historicalMain, activeSubs, legacySubs } = useMemo(() => { + // 数据分类 — 🚀 虚拟扁平化:三层+嵌套协助统一挂在视觉主干下 + const { activeMain, historicalMain, subsByRootMain, rootMainMap } = useMemo(() => { ALL_TASKS.clear(); if (tasks.length) collectAll(tasks); const all = Array.from(ALL_TASKS); - const am: TaskResponse[] = []; const hm: TaskResponse[] = []; const as: TaskResponse[] = []; const ls: TaskResponse[] = []; + // 拍平映射 + 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[] = []; for (const t of all) { if (isMain(t)) { if (active(t.status)) am.push(t); else hm.push(t); - } else { - if (active(t.status)) { - const p = findParent(t); - if (p && active(p.status)) as.push(t); else ls.push(t); - } + srm[t.id] = []; } } + // 非主线任务:按根主干分组 + 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()); - return { activeMain: am, historicalMain: hm, activeSubs: as, legacySubs: ls }; + // 🚀 层级排序:直接子节点优先,嵌套子节点跟在父节点之后 + for (const key of Object.keys(srm)) { + const subs = srm[key]; + // 构建父子索引 + const childMap: Record = {}; + for (const s of subs) { + const pid = s.parent_task_id || ''; + if (!childMap[pid]) childMap[pid] = []; + childMap[pid].push(s); + } + Object.values(childMap).forEach(arr => arr.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())); + // 递归收集:父 → 子 → 孙 + const ordered: TaskResponse[] = []; + const collect = (parentId: string) => { + const kids = childMap[parentId] || []; + for (const k of kids) { + ordered.push(k); + collect(k.id); + } + }; + collect(key); // 从根主干开始收集 + srm[key] = ordered; + } + return { activeMain: am, historicalMain: hm, subsByRootMain: srm, rootMainMap: rootMap }; }, [tasks]); const allMains = [...historicalMain, ...activeMain]; @@ -130,25 +177,19 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren {/* ─── 焦点模式 ─── */} {!showFullMap && (
- {legacySubs.length > 0 && ( -
-

⚠ {legacySubs.length} 个遗留分支仍在进行中

-
- )} {activeMain.map(mainTask => { - const allSubs = [...activeSubs, ...legacySubs]; - const leftSubs = allSubs.filter((_, i) => i % 2 === 0); - const rightSubs = allSubs.filter((_, i) => i % 2 === 1); + const children = subsByRootMain[mainTask.id] || []; + const leftSubs = children.filter((_, i) => i % 2 === 0); + const rightSubs = children.filter((_, i) => i % 2 === 1); return (
- {/* 左翼:flex-col 纵向堆叠,右对齐 */} + {/* 左翼 */}
{leftSubs.map(t => { - const isLegacy = legacySubs.includes(t); + const isLegacy = !active(mainTask.status); return (
- - {/* 左翼:箭头 ◀ 指向分支卡片(背离中央主干) */} +
@@ -161,18 +202,17 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
- {/* 右翼:flex-col 纵向堆叠,左对齐 */} + {/* 右翼 */}
{rightSubs.map(t => { - const isLegacy = legacySubs.includes(t); + const isLegacy = !active(mainTask.status); return (
- {/* 右翼:箭头 ▶ 指向分支卡片(背离中央主干) */}
- +
); })} @@ -188,7 +228,7 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren {showFullMap && (
{allMains.map(mainTask => { - const children = Array.from(ALL_TASKS).filter(t => t.parent_task_id === mainTask.id); + const children = subsByRootMain[mainTask.id] || []; const hasLegacyActive = children.some(c => !isMain(c) && !active(mainTask.status)); const leftChildren = children.filter((_, i) => i % 2 === 0); const rightChildren = children.filter((_, i) => i % 2 === 1); @@ -196,16 +236,14 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren return (
- {/* 垂直轴线贯穿中央 */}
-
- {/* 左翼:flex-col */} + {/* 左翼 */}
{leftChildren.map(c => (
+ assigneeName={assigneeNames?.[c.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} rootMainId={mainTask.id} />
@@ -221,7 +259,7 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
)}
- {/* 右翼:flex-col */} + {/* 右翼 */}
{rightChildren.map(c => (
@@ -230,7 +268,7 @@ export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, curren
+ assigneeName={assigneeNames?.[c.assignee_id || ""]} onAction={onAction} currentUser={currentUser} onViewRecords={setRecordsTask} rootMainId={mainTask.id} />
))}