/** * 流转树双模式可视化 — 焦点模式 + 全景模式 */ import { memo, useMemo, useState } from "react"; import { GitBranch, AlertTriangle, Clock, CheckCircle, Flag, FileText, X } from "lucide-react"; import type { TaskResponse } from "../../types/api"; import { TASK_STATUS } from "../../types/api"; import { getStatusConfig } from "../../constants/task"; 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 }) { return ; } /** 微型左箭头 SVG */ function ArrowLeft({ color = "#9ca3af" }: { color?: string }) { return ; } function parseImages(s: string | null | undefined): string[] { if (!s) return []; try { return JSON.parse(s); } catch { return []; } } function imageUrl(u: string) { if (!u) return ""; return u.startsWith("http") ? u : import.meta.env.VITE_API_BASE_URL + (u.startsWith("/") ? u : "/" + u); } const ALL_TASKS = new Set(); function collectAll(tasks: TaskResponse[]) { tasks.forEach(t => { ALL_TASKS.add(t); if (t.child_tasks) collectAll(t.child_tasks); }); } function findParent(child: TaskResponse): TaskResponse | undefined { for (const t of ALL_TASKS) { if (t.id === child.parent_task_id) return t; } return undefined; } // ============================================================ // 极简卡片 // ============================================================ const SlimCard = memo(function SlimCard({ 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 (
{main ? "主线" : "分支"}

{task.task_name}

{cfg.label} {assigneeName || task.assignee_id || "—"}
{/* 单行时间 */}

⏰ {fmtTime(task.created_at).split(" ")[0]} {task.completed_at ? ` → ${fmtTime(task.completed_at).split(" ")[0]}` : " → 至今"}

{legacy &&

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

} {isNestedSpawn &&

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

} {/* 操作按钮 */} {active && isOwner && (
{task.status?.toUpperCase() === TASK_STATUS.PENDING && } {task.status?.toUpperCase() !== TASK_STATUS.PENDING && }
)} {active && !isOwner && isManager && (
)} {/* 记录 */} {task.records && task.records.length > 0 && (
{ e.stopPropagation(); onViewRecords?.(task); }} className="mt-1 cursor-pointer rounded bg-blue-50 px-1.5 py-0.5 text-[8px] text-blue-600 hover:bg-blue-100"> {task.records.length}条
)}
); }); // ============================================================ // 主视图 // ============================================================ interface TaskFlowViewProps { tasks: TaskResponse[]; onAction: (t: ModalTarget) => void; currentUser?: { username?: string; role?: string } | null; assigneeNames?: Record; } export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, currentUser, assigneeNames }: TaskFlowViewProps) { const [showFullMap, setShowFullMap] = useState(false); const [recordsTask, setRecordsTask] = useState(null); // 数据分类 — 🚀 虚拟扁平化:三层+嵌套协助统一挂在视觉主干下 const { activeMain, historicalMain, subsByRootMain, rootMainMap } = 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[] = []; for (const t of all) { if (isMain(t)) { if (active(t.status)) am.push(t); else hm.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()); // 🚀 层级排序:直接子节点优先,嵌套子节点跟在父节点之后 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]; return (
{/* 模式切换 */}
{/* ─── 焦点模式 ─── */} {!showFullMap && (
{activeMain.map(mainTask => { const children = subsByRootMain[mainTask.id] || []; const leftSubs = children.filter((_, i) => i % 2 === 0); const rightSubs = children.filter((_, i) => i % 2 === 1); return (
{/* 左翼 */}
{leftSubs.map(t => { const isLegacy = !active(mainTask.status); return (
); })}
{/* 中央主干 */}
{/* 右翼 */}
{rightSubs.map(t => { const isLegacy = !active(mainTask.status); return (
); })}
); })} {activeMain.length === 0 &&

当前无活跃主线任务

}
)} {/* ─── 全景模式 ─── */} {showFullMap && (
{allMains.map(mainTask => { 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); const lineStyle = hasLegacyActive && !active(mainTask.status); return (
{/* 左翼 */}
{leftChildren.map(c => (
))}
{/* 中央 */}
{active(mainTask.status) && (
)}
{/* 右翼 */}
{rightChildren.map(c => (
))}
{allMains.indexOf(mainTask) < allMains.length - 1 && (
▼
)}
); })} {allMains.length === 0 &&

暂无流转记录

}
)} {/* 记录弹窗 */} {recordsTask && (
setRecordsTask(null)} />

提交记录 — {recordsTask.task_name}

{(recordsTask.records || []).length === 0 ?

暂无记录

:
{[...recordsTask.records!].reverse().map((r, i) => (

{fmtTime(r.created_at)}

{(r.note || r.remark) &&

{r.note || r.remark}

} {(() => { const imgs = parseImages((r as any).images); if (!imgs.length) return null; return
{imgs.map((img, j) => window.open(imageUrl(img))} />)}
; })()}
))}
}
)}
); }); export default TaskFlowView;