/** * 任务流转卡片堆叠视图 — 水平分支 + 卡片层叠布局 * * 与移动端 TaskSwipeCards.vue 核心逻辑完全对齐: * - 任务树按 TRANSFER/SPAWN 分类为主线/分支 * - 状态标签、时间格式、卡片元素一致 * - 三种权限模式(本人/管理员/只读) */ import { memo, useMemo } from "react"; import { GitBranch, ArrowDown, AlertTriangle, Clock, CheckCircle, Flag, FileText, } 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"; // ============================================================ // 工具函数 — 与移动端 fmtTime/statusLabel 完全一致 // ============================================================ 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())}`; } /** 判断任务是否为主线(与移动端 isMain 逻辑一致) */ function isMainTask(t: TaskResponse): boolean { return !t.parent_task_id || t.task_type === "TRANSFER" || t.task_type === "RECOVERY" || (!t.task_type && !!t.parent_task_id); } // ============================================================ // 类型 // ============================================================ interface LevelGroup { depth: number; tasks: TaskResponse[]; } // ============================================================ // 任务树解析 — 按 depth 拍平(与移动端 lanes 逻辑结构等价) // ============================================================ 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) { result.push(...flattenLevels(t.child_tasks, depth + 1)); } } return result; } function groupByDepth(levels: LevelGroup[]): Map { const map = new Map(); for (const lvl of levels) { if (!map.has(lvl.depth)) map.set(lvl.depth, []); const existing = map.get(lvl.depth)!; for (const t of lvl.tasks) { if (!existing.find((e) => e.id === t.id)) existing.push(t); } } return map; } // ============================================================ // 单张任务卡片 — 与移动端 ss-card 完全对齐 // ============================================================ const FlowCard = memo(function FlowCard({ task, isActive, onAction, currentUser, assigneeName, }: { task: TaskResponse; isActive: boolean; onAction: (target: ModalTarget) => void; currentUser?: { username?: string; role?: string } | null; assigneeName?: string; }) { 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; const isCanceled = task.status?.toUpperCase() === "CANCELED"; const main = isMainTask(task); // 权限:本人或管理员 const isOwner = !!(currentUser?.username && task.assignee_id === currentUser.username); const isManager = currentUser?.role === "SUPER_ADMIN" || currentUser?.role === "SUPERVISOR"; const canOperate = isOwner || isManager; return (
{/* 主线/分支 角标 — 与移动端 tc-ribbon 一致 */}
{main ? "主分支" : "分支"}
{/* 返工/入库标记 — 与移动端 tag-rework-sm 一致 */}
{task.is_rework && ( 返工 )} {task.status === "ARCHIVED" && ( 📦 入库 )}
{/* 头部:类型标签 + 状态 — 与移动端 tc-head 一致 */}
{main ? "主分支" : "分支"} {task.child_tasks.length > 1 && ( 裂变×{task.child_tasks.length} )}
{cfg.label}
{/* 任务名 — 与移动端 tc-name 一致 */}

{task.task_name}

{/* 负责人 — 与移动端 tc-meta 一致 */}
👤 负责人 {assigneeName || task.assignee_id || "未分配"}
{/* 备注 — 与移动端 tc-remark-box 一致 */} {task.remark && (

📌 备注

{task.remark}

)} {/* 提交记录 — 与移动端 tc-records-link 一致 */} {task.records && task.records.length > 0 && (
共 {task.records.length} 条提交记录
)} {/* 停留耗时 — 与移动端逻辑一致 */} {dwell && (

{dwell.highlight ? ⏳ 停留: {dwell.text} : 耗时: {dwell.text}}

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

驳回原因: {task.reject_reason}

)} {/* 状态标记 — 与移动端 tc-stats 一致 */}
{task.received_at && ( 已接收 )} {task.completed_at && ( 已完工 )}
{/* 时间 — 与移动端 fmtTime 格式一致 */}

创建: {fmtTime(task.created_at)}

{task.received_at &&

接收: {fmtTime(task.received_at)}

} {task.completed_at &&

完工: {fmtTime(task.completed_at)}

}
{/* 操作按钮 — 三种权限模式 */} {isActive && canOperate && (
{task.status?.toUpperCase() === TASK_STATUS.PENDING && ( <> )} {task.status?.toUpperCase() === TASK_STATUS.WIP && ( <> )}
)} {isActive && !isOwner && isManager && (

⚠ 运维干预模式

)} {isActive && !canOperate && (

非当前任务责任人,无法操作

)}
); }); // ============================================================ // 停留耗时 — 与移动端逻辑一致 // ============================================================ 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 TaskFlowViewProps { tasks: TaskResponse[]; onAction: (target: ModalTarget) => void; currentUser?: { username?: string; role?: string } | null; assigneeNames?: Record; } export const TaskFlowView = memo(function TaskFlowView({ tasks, onAction, currentUser, assigneeNames }: TaskFlowViewProps) { const depthMap = useMemo(() => { if (!tasks || tasks.length === 0) return new Map(); return groupByDepth(flattenLevels(tasks)); }, [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;