/** * 流转树双模式可视化 — 焦点模式 + 全景模式 */ 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"; } 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: any): string[] { if (!s) return []; if (Array.isArray(s)) return s; // Pydantic 序列化后的数组 if (typeof s === "string") { try { return JSON.parse(s); } catch { return []; } } 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); // 数据分类 — 🚀 仅根级主干作为垂直时间线节点,子节点通过 childMap 分支递归渲染 const { allMains, childMap } = useMemo(() => { ALL_TASKS.clear(); if (tasks.length) collectAll(tasks); const all = Array.from(ALL_TASKS); // 🚀 收集所有主线任务(全部进入中央垂直主轴) const mains: TaskResponse[] = []; for (const t of all) { if (isMain(t)) mains.push(t); } 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) { const pid = t.parent_task_id || ''; if (!childMap[pid]) childMap[pid] = []; 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 { allMains: mains, childMap }; }, [tasks]); // 🚀 递归渲染分支节点 — 每个节点从自己的 childMap 获取直系子孙,保持树结构不断裂 const renderBranch = (node: TaskResponse, side: 'left' | 'right', isLegacy: boolean, rootMainId: string): JSX.Element => { const kids = childMap[node.id] || []; const arrow = side === 'left' ? (
) : (
); const card = ; const kidsContainer = kids.length > 0 ? (
{kids.map(k => renderBranch(k, side, isLegacy, rootMainId))}
) : null; return (
{side === 'left' && kidsContainer} {side === 'left' && card} {arrow} {side === 'right' && card} {side === 'right' && kidsContainer}
); }; // 🚀 焦点模式 vs 全景模式:共用同一套垂直时间线布局,仅数据过滤不同 const visibleMains = useMemo(() => { if (showFullMap) return allMains; // 焦点模式:过滤掉已完成/已入库的历史主线,保留活跃主线 + 所有协助分支 return allMains.filter(t => active(t.status)); }, [allMains, showFullMap]); return (
{/* 模式切换 */}
{/* ─── 统一垂直时间线布局(焦点/全景共用) ─── */}
{visibleMains.map(mainTask => { // 🔧 侧翼严格过滤:主线归主轴,仅 SPAWN 协助分支进入左右翼 const directChildren = (childMap[mainTask.id] || []).filter(c => !isMain(c)); 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 && (
▼
)}
); })} {visibleMains.length === 0 && (

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

)}
{/* 记录弹窗 */} {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.images); if (!imgs.length) return null; return
{imgs.map((img, j) => window.open(imageUrl(img))} />)}
; })()}
))}
}
)}
); }); export default TaskFlowView;