/** * 任务流转卡片堆叠视图 — 水平分支 + 卡片层叠布局 * * 将递归任务树按层级拆分为水平分支,每层卡片横向排列, * 当前激活(WIP/PENDING)卡片高亮居中,操作按钮集成在卡片底部。 */ import { memo, useMemo } from "react"; import { GitBranch, ArrowDown, AlertTriangle, Clock, } 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 calcDwell( receivedAt: string | null, completedAt: string | null, status: string, ): { text: string; highlight: boolean } | null { if (!receivedAt) return null; const start = new Date(receivedAt).getTime(); const end = completedAt ? new Date(completedAt).getTime() : Date.now(); const diffMs = end - start; if (diffMs < 0) return null; const totalMin = Math.floor(diffMs / 60000); if (totalMin < 1) return { text: "< 1分钟", highlight: status === "WIP" }; if (totalMin < 60) return { text: `${totalMin}分钟`, highlight: status === "WIP" }; const hours = Math.floor(totalMin / 60); const remainMin = totalMin % 60; if (hours < 24) { return { text: `${hours}小时${remainMin > 0 ? remainMin + "分钟" : ""}`, highlight: status === "WIP" }; } const days = Math.floor(hours / 24); const remainHr = hours % 24; return { text: `${days}天${remainHr > 0 ? remainHr + "小时" : ""}`, highlight: status === "WIP" }; } // ---- 类型 ---- interface LevelGroup { depth: number; tasks: TaskResponse[]; } // ---- 将递归树拍平为层级 ---- function flattenLevels(tasks: TaskResponse[], depth: number = 0): LevelGroup[] { const result: LevelGroup[] = []; if (!tasks || tasks.length === 0) return result; // 当前层级 result.push({ depth, tasks }); // 递归子层级 for (const t of tasks) { if (t.child_tasks && t.child_tasks.length > 0) { const childLevels = flattenLevels(t.child_tasks, depth + 1); for (const cl of childLevels) { // 合并同深度的层级 const existing = result.find((r) => r.depth === cl.depth && r !== result[result.indexOf({ depth, tasks })] ); // 简化:直接 push,在渲染时按 depth 分组 } result.push(...childLevels); } } return result; } /** 按 depth 聚合所有层级 */ function groupByDepth(levels: LevelGroup[]): Map { const map = new Map(); for (const lvl of levels) { if (!map.has(lvl.depth)) map.set(lvl.depth, []); map.get(lvl.depth)!.push(...lvl.tasks); } return map; } // ---- 单张任务卡片 ---- const FlowCard = memo(function FlowCard({ task, isActive, onAction, }: { task: TaskResponse; isActive: boolean; onAction: (target: ModalTarget) => void; }) { const cfg = getStatusConfig(task.status); const dwell = calcDwell(task.received_at, task.completed_at, task.status); const isCompleted = task.status?.toUpperCase() === TASK_STATUS.COMPLETED; const isArchived = task.status?.toUpperCase() === TASK_STATUS.ARCHIVED; return (
{/* 返工标记 */} {task.is_rework && (
返工
)} {/* 裂变标记 */} {task.child_tasks.length > 1 && (
裂变×{task.child_tasks.length}
)} {/* 任务名 */}

{task.task_name}

{/* 状态 Badge */}
{cfg.label} {task.assignee_id && ( {task.assignee_id} )}
{/* 停留耗时 */} {dwell && (

{dwell.highlight ? ⏳ {dwell.text} : {dwell.text}}

)} {/* 驳回原因 */} {task.reject_reason && (

{task.reject_reason}

)} {/* 日期 */}

{task.received_at && `接收: ${new Date(task.received_at).toLocaleDateString("zh-CN")}`}

{/* 操作按钮 — 仅激活态显示 */} {isActive && (
{task.status?.toUpperCase() === TASK_STATUS.PENDING && ( <> )} {task.status?.toUpperCase() === TASK_STATUS.WIP && ( <> )}
)}
); }); // ---- 主视图 ---- interface TaskFlowViewProps { tasks: TaskResponse[]; onAction: (target: ModalTarget) => void; } export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction }: TaskFlowViewProps) { // 按 depth 分组的层级数据 const depthMap = useMemo(() => { if (!tasks || tasks.length === 0) return new Map(); const flat = flattenLevels(tasks); // 按 depth 聚合去重 const merged = new Map>(); for (const lvl of flat) { if (!merged.has(lvl.depth)) merged.set(lvl.depth, new Map()); const inner = merged.get(lvl.depth)!; for (const t of lvl.tasks) { if (!inner.has(t.id)) inner.set(t.id, t); } } const result = new Map(); for (const [depth, idMap] of merged) { result.set(depth, Array.from(idMap.values())); } return result; }, [tasks]); const depths = Array.from(depthMap.keys()).sort((a, b) => a - b); if (depths.length === 0) return null; return (
{depths.map((depth) => { const levelTasks = depthMap.get(depth) || []; return (
{/* 层级标签 */}
{depth === 0 ? "顶层任务" : `第 ${depth} 层子任务`}
{/* 卡片横向排列 */}
{levelTasks.map((task) => { const isActive = task.status?.toUpperCase() === TASK_STATUS.PENDING || task.status?.toUpperCase() === TASK_STATUS.WIP; return (
); })}
{/* 层级间连接箭头 */} {depth < depths.length - 1 && (
)}
); })}
); }); export default TaskFlowView;