Przeglądaj źródła

修复部分作业详情和流程设计保存按钮问题

pm 4 miesięcy temu
rodzic
commit
0b0b98056e
2 zmienionych plików z 724 dodań i 5 usunięć
  1. 225 4
      src/components/ProcessDesigner.tsx
  2. 499 1
      src/components/WorkJobDetail.tsx

+ 225 - 4
src/components/ProcessDesigner.tsx

@@ -885,6 +885,11 @@ export default function ProcessDesigner() {
   const [nodes, setNodes, onNodesChange] = useNodesState([]);
   const [edges, setEdges, onEdgesChange] = useEdgesState([]);
   
+  // 保存初始状态,用于检测是否有未保存的更改
+  const initialNodesRef = useRef<Node[]>([]);
+  const initialEdgesRef = useRef<Edge[]>([]);
+  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
+  
   // 从URL参数获取流程ID
   const workflowId = React.useMemo(() => {
     const params = new URLSearchParams(location.search);
@@ -1056,6 +1061,20 @@ export default function ProcessDesigner() {
     }
   }, [getCacheKey]);
 
+  // 清除当前流程的所有节点缓存
+  const clearCurrentWorkflowNodeCache = useCallback(() => {
+    if (!nodes || nodes.length === 0) return;
+    try {
+      nodes.forEach((node) => {
+        const nodeCacheKey = `${cachePrefix}${node.id}`;
+        sessionStorage.removeItem(nodeCacheKey);
+      });
+      console.log('当前流程的节点缓存已清除');
+    } catch (error) {
+      console.error('清除节点缓存失败:', error);
+    }
+  }, [nodes]);
+
   // 比较两个 JSON 字符串内容是否相同(忽略格式差异)
   const compareJsonContent = useCallback((json1: string | null, json2: string | null): boolean => {
     if (!json1 || !json2) return false;
@@ -1070,18 +1089,101 @@ export default function ProcessDesigner() {
     }
   }, []);
 
+  // 比较当前状态和初始状态,检测是否有未保存的更改
+  const checkUnsavedChanges = useCallback(() => {
+    // 比较节点数量
+    if (nodes.length !== initialNodesRef.current.length) {
+      return true;
+    }
+    // 比较边数量
+    if (edges.length !== initialEdgesRef.current.length) {
+      return true;
+    }
+    
+    // 创建初始节点的映射,方便通过ID查找
+    const initialNodesMap = new Map(initialNodesRef.current.map(n => [n.id, n]));
+    const initialEdgesMap = new Map(initialEdgesRef.current.map(e => [e.id, e]));
+    
+    // 深度比较节点(包括位置、数据等)
+    const nodesChanged = nodes.some((node) => {
+      const initialNode = initialNodesMap.get(node.id);
+      if (!initialNode) return true; // 新节点
+      
+      // 比较基本属性
+      if (node.type !== initialNode.type ||
+          node.position.x !== initialNode.position.x ||
+          node.position.y !== initialNode.position.y) {
+        return true;
+      }
+      
+      // 比较节点数据(包括缓存数据)
+      const cachedData = loadNodeCache(node.id);
+      const mergedData = cachedData ? { ...node.data, ...cachedData } : node.data;
+      // 对于初始节点,我们只比较初始数据,不考虑初始时的缓存
+      const initialMergedData = initialNode.data;
+      
+      return JSON.stringify(mergedData) !== JSON.stringify(initialMergedData);
+    });
+    
+    if (nodesChanged) {
+      return true;
+    }
+    
+    // 检查是否有节点被删除
+    const hasDeletedNode = initialNodesRef.current.some(initialNode => 
+      !nodes.find(n => n.id === initialNode.id)
+    );
+    if (hasDeletedNode) {
+      return true;
+    }
+    
+    // 深度比较边
+    const edgesChanged = edges.some((edge) => {
+      const initialEdge = initialEdgesMap.get(edge.id);
+      if (!initialEdge) return true; // 新边
+      
+      return edge.source !== initialEdge.source ||
+             edge.target !== initialEdge.target ||
+             edge.sourceHandle !== initialEdge.sourceHandle ||
+             edge.targetHandle !== initialEdge.targetHandle ||
+             edge.type !== initialEdge.type;
+    });
+    
+    if (edgesChanged) {
+      return true;
+    }
+    
+    // 检查是否有边被删除
+    const hasDeletedEdge = initialEdgesRef.current.some(initialEdge => 
+      !edges.find(e => e.id === initialEdge.id)
+    );
+    
+    return hasDeletedEdge;
+  }, [nodes, edges, loadNodeCache]);
+
   // 调试:监听 edges 变化
   useEffect(() => {
     console.log('Edges 状态更新:', edges.length, edges);
   }, [edges]);
 
-  // 监听 nodes 和 edges 变化,实时保存到缓存
+  // 监听 nodes 和 edges 变化,实时保存到缓存,并检测是否有未保存的更改
   useEffect(() => {
     // 只有当有节点或边时才保存缓存(避免初始化时保存空数据)
     if (workflowId && (nodes.length > 0 || edges.length > 0)) {
       saveWorkflowCache(workflowId, nodes, edges);
     }
-  }, [nodes, edges, workflowId, saveWorkflowCache]);
+    
+    // 检测是否有未保存的更改(只有在初始状态已设置后才检测)
+    // 如果初始状态和当前状态都为空,则没有未保存的更改
+    if ((initialNodesRef.current.length > 0 || initialEdgesRef.current.length > 0) || 
+        (nodes.length > 0 || edges.length > 0)) {
+      const hasChanges = checkUnsavedChanges();
+      setHasUnsavedChanges(hasChanges);
+    } else {
+      // 如果初始状态和当前状态都为空,则没有未保存的更改
+      setHasUnsavedChanges(false);
+    }
+  }, [nodes, edges, workflowId, saveWorkflowCache, checkUnsavedChanges]);
 
   // 获取隔离方式字典数据
   const getIsolationMethodDictList = async () => {
@@ -1308,6 +1410,10 @@ export default function ProcessDesigner() {
       if (updateHistory) {
         setHistory([{ nodes: importedNodes, edges: importedEdges }]);
         setHistoryIndex(0);
+        // 更新初始状态,用于检测未保存的更改
+        initialNodesRef.current = JSON.parse(JSON.stringify(importedNodes));
+        initialEdgesRef.current = JSON.parse(JSON.stringify(importedEdges));
+        setHasUnsavedChanges(false);
       }
 
       return true;
@@ -2008,6 +2114,11 @@ export default function ProcessDesigner() {
       
       // 保存成功后清除缓存
       clearWorkflowCache(workflowId);
+      
+      // 更新初始状态
+      initialNodesRef.current = JSON.parse(JSON.stringify(nodes));
+      initialEdgesRef.current = JSON.parse(JSON.stringify(edges));
+      setHasUnsavedChanges(false);
     } catch (error: any) {
       console.error('保存流程失败:', error);
       message.error(error?.message || '流程保存失败');
@@ -2094,6 +2205,11 @@ export default function ProcessDesigner() {
       // 保存成功后清除缓存
       clearWorkflowCache(workflowId);
       
+      // 更新初始状态
+      initialNodesRef.current = JSON.parse(JSON.stringify(nodes));
+      initialEdgesRef.current = JSON.parse(JSON.stringify(edges));
+      setHasUnsavedChanges(false);
+      
       // 显示成功提示
       message.success('流程保存成功');
       
@@ -2110,8 +2226,108 @@ export default function ProcessDesigner() {
 
   // 返回
   const handleBack = () => {
-    // 导航到 dashboard,Dashboard 会从 sessionStorage 读取上次的菜单状态
-    navigate('/dashboard');
+    // 检查是否有未保存的更改
+    if (hasUnsavedChanges) {
+      Modal.warning({
+        title: '提示',
+        content: '检测到您有未保存的更改,请先保存后再返回。',
+        okText: '保存',
+        maskClosable: false,
+        closable: false,
+        onOk: async () => {
+          // 调用保存函数
+          try {
+            // 构建导出数据(与handleSave中的逻辑相同)
+            const adjacency: Record<string, { parentUuid: string[]; childrenUuid: string[] }> = {};
+            const nodeIdMap = new Map<string, string>();
+            nodes.forEach(n => {
+              nodeIdMap.set(n.id, n.id);
+            });
+
+            nodes.forEach(n => {
+              const uuid = nodeIdMap.get(n.id) || n.id;
+              adjacency[uuid] = adjacency[uuid] || { parentUuid: [], childrenUuid: [] };
+            });
+            edges.forEach(e => {
+              const sourceUuid = nodeIdMap.get(e.source) || e.source;
+              const targetUuid = nodeIdMap.get(e.target) || e.target;
+              if (!adjacency[sourceUuid]) adjacency[sourceUuid] = { parentUuid: [], childrenUuid: [] };
+              if (!adjacency[targetUuid]) adjacency[targetUuid] = { parentUuid: [], childrenUuid: [] };
+              adjacency[sourceUuid].parentUuid.push(targetUuid);
+              adjacency[targetUuid].childrenUuid.push(sourceUuid);
+            });
+
+            const exportData = {
+              nodeCount: nodes.length,
+              edgeCount: edges.length,
+              adjacency,
+              nodes: nodes.map(n => {
+                const cachedData = loadNodeCache(n.id);
+                const mergedData = cachedData 
+                  ? { ...n.data, ...cachedData }
+                  : n.data;
+                
+                const nodeObj: any = {
+                  uuid: n.id,
+                  type: n.type,
+                  position: n.position,
+                  nodeName: mergedData.label || '',
+                  nodeIcon: mergedData.icon || mergedData.type || n.type || '',
+                  data: mergedData,
+                };
+                return nodeObj;
+              }),
+              edges: edges.map(e => {
+                return {
+                  id: e.id,
+                  source: nodeIdMap.get(e.source) || e.source,
+                  target: nodeIdMap.get(e.target) || e.target,
+                  sourceHandle: e.sourceHandle,
+                  targetHandle: e.targetHandle,
+                  type: e.type,
+                };
+              }),
+            };
+
+            const content = JSON.stringify(exportData, null, 2);
+            
+            if (!workflowId) {
+              message.warning('无法保存:缺少流程ID,请先创建流程');
+              return;
+            }
+
+            const name = workflowDetail?.name || '';
+            const description = workflowDetail?.description || '';
+
+            await workflowDesignApi.updateWorkflowDesign({
+              id: workflowId,
+              content: content,
+              name: name,
+              description: description,
+            });
+
+            clearWorkflowCache(workflowId);
+            message.success('流程保存成功');
+            
+            // 更新初始状态
+            initialNodesRef.current = JSON.parse(JSON.stringify(nodes));
+            initialEdgesRef.current = JSON.parse(JSON.stringify(edges));
+            setHasUnsavedChanges(false);
+            
+            // 保存成功后返回
+            setTimeout(() => {
+              navigate('/dashboard');
+            }, 500);
+          } catch (error: any) {
+            console.error('保存流程失败:', error);
+            message.error(error?.message || '流程保存失败');
+          }
+        },
+      });
+    } else {
+      // 没有未保存的更改,直接返回
+      navigate('/dashboard');
+    }
   };
 
   // 缩放控制
@@ -2393,6 +2609,11 @@ export default function ProcessDesigner() {
       setHistory(newHistory);
       setHistoryIndex(0);
 
+      // 更新初始状态,用于检测未保存的更改
+      initialNodesRef.current = JSON.parse(JSON.stringify(importedNodes));
+      initialEdgesRef.current = JSON.parse(JSON.stringify(importedEdges));
+      setHasUnsavedChanges(false);
+
       toast.success('流程导入成功');
       setImportVisible(false);
       setImportJson('');

+ 499 - 1
src/components/WorkJobDetail.tsx

@@ -16,6 +16,8 @@ import {
   Minus,
   Loader2
 } from 'lucide-react';
+import { Timeline, Badge, Card, Collapse, Tag } from 'antd';
+import { CheckCircleOutlined, ClockCircleOutlined, SyncOutlined } from '@ant-design/icons';
 import { workJobApi, WorkJobVO, WorkflowWorkNodeDO } from '../api/WorkJob';
 import { formatDateWithFormat } from '../utils/formatTime';
 
@@ -162,6 +164,487 @@ const getNodeStatus = (workNode: WorkflowWorkNodeDO, nodeMap: Map<string, Workfl
   return 'pending';
 };
 
+// 工作流渲染组件
+interface WorkflowRendererProps {
+  nodeList: WorkflowWorkNodeDO[];
+  expandedBranchGroups: Set<string>;
+  setExpandedBranchGroups: React.Dispatch<React.SetStateAction<Set<string>>>;
+  getExecutorInfo: (workNode: WorkflowWorkNodeDO, type: string) => string;
+  getNodeIcon: (type: string, status: 'completed' | 'in_progress' | 'pending') => React.ReactNode;
+  formatDate: (date: string | number | Date) => string;
+}
+
+const WorkflowRenderer: React.FC<WorkflowRendererProps> = ({
+  nodeList,
+  expandedBranchGroups,
+  setExpandedBranchGroups,
+  getExecutorInfo,
+  getNodeIcon,
+  formatDate,
+}) => {
+  // 用于控制节点依次渲染的状态
+  const [visibleNodes, setVisibleNodes] = useState<Set<string>>(new Set());
+  
+  useEffect(() => {
+    // 重置可见节点
+    setVisibleNodes(new Set());
+    
+    // 依次显示节点,每个节点延迟 200ms
+    const timers: NodeJS.Timeout[] = [];
+    
+    const sortedNodes = [...nodeList].sort((a, b) => {
+      const aId = a.id || 0;
+      const bId = b.id || 0;
+      if (aId !== bId) return aId - bId;
+      const aTime = a.createTime ? new Date(a.createTime).getTime() : 0;
+      const bTime = b.createTime ? new Date(b.createTime).getTime() : 0;
+      return aTime - bTime;
+    });
+    
+    sortedNodes.forEach((node, index) => {
+      const timer = setTimeout(() => {
+        setVisibleNodes(prev => new Set([...prev, node.uuid || String(node.id || index)]));
+      }, index * 200 + 100);
+      timers.push(timer);
+    });
+    
+    return () => {
+      timers.forEach(timer => clearTimeout(timer));
+    };
+  }, [nodeList]);
+  // 按 parentUuid 分组,找出同父节点的子节点
+  const branchGroups = new Map<string, WorkflowWorkNodeDO[]>();
+  nodeList.forEach((node) => {
+    if (node.parentUuid) {
+      if (!branchGroups.has(node.parentUuid)) {
+        branchGroups.set(node.parentUuid, []);
+      }
+      branchGroups.get(node.parentUuid)!.push(node);
+    }
+  });
+
+  // 调试:打印所有分组信息
+  console.log('=== 节点分组信息 ===');
+  branchGroups.forEach((children, parentUuid) => {
+    console.log(`父节点 ${parentUuid} 有 ${children.length} 个子节点:`, children.map(c => ({ uuid: c.uuid, nodeName: c.nodeName })));
+  });
+
+  // 对每个分支组进行排序(按id或创建时间)
+  branchGroups.forEach((children, parentUuid) => {
+    children.sort((a, b) => {
+      const aId = a.id || 0;
+      const bId = b.id || 0;
+      if (aId !== bId) return aId - bId;
+      const aTime = a.createTime ? new Date(a.createTime).getTime() : 0;
+      const bTime = b.createTime ? new Date(b.createTime).getTime() : 0;
+      return aTime - bTime;
+    });
+  });
+
+  // 找出有多个子节点的父节点(需要显示分支的)
+  const parentNodesWithBranches = new Set<string>();
+  branchGroups.forEach((children, parentUuid) => {
+    if (children.length > 1) {
+      parentNodesWithBranches.add(parentUuid);
+    }
+  });
+
+  // 构建节点映射,方便查找父节点
+  const nodeMap = new Map<string, WorkflowWorkNodeDO>();
+  nodeList.forEach((node) => {
+    if (node.uuid) {
+      nodeMap.set(node.uuid, node);
+    }
+  });
+
+  // 构建主流程节点列表
+  // 如果节点有多个子节点,在主流程中显示父节点,分支节点隐藏
+  const mainFlowNodes: (WorkflowWorkNodeDO & { isBranchParent?: boolean; branchNodes?: WorkflowWorkNodeDO[]; branchParentUuid?: string; mainNodeUuid?: string })[] = [];
+  const processedNodes = new Set<string>();
+  const allBranchNodeUuids = new Set<string>(); // 记录所有有相同父节点的节点uuid
+
+  // 标记所有有相同父节点的节点(无论数量多少)
+  branchGroups.forEach((children, parentUuid) => {
+    if (children.length > 1) {
+      children.forEach((child) => {
+        allBranchNodeUuids.add(child.uuid || '');
+      });
+    }
+  });
+
+  // 按顺序遍历节点列表,构建主流程
+  // 先按id或创建时间排序,确保顺序正确
+  const sortedNodeList = [...nodeList].sort((a, b) => {
+    const aId = a.id || 0;
+    const bId = b.id || 0;
+    if (aId !== bId) return aId - bId;
+    const aTime = a.createTime ? new Date(a.createTime).getTime() : 0;
+    const bTime = b.createTime ? new Date(b.createTime).getTime() : 0;
+    return aTime - bTime;
+  });
+
+  sortedNodeList.forEach((node) => {
+    // 如果节点已经被处理过,直接跳过
+    if (processedNodes.has(node.uuid || '')) {
+      console.log(`节点 ${node.uuid} 已被处理,跳过`);
+      return;
+    }
+
+    // 检查这个节点是否有相同父节点的兄弟节点
+    if (node.parentUuid) {
+      const siblings = branchGroups.get(node.parentUuid) || [];
+      
+      // 如果有多个同父节点的子节点
+      if (siblings.length > 1) {
+        // 如果是第一个节点(按排序后的顺序),需要处理
+        const firstSibling = siblings[0];
+        if (firstSibling && firstSibling.uuid === node.uuid && !processedNodes.has(node.parentUuid)) {
+          console.log(`处理分支组: 父节点 ${node.parentUuid}, 子节点数量: ${siblings.length}`, siblings.map(s => s.uuid));
+          processedNodes.add(node.parentUuid);
+          // 标记所有兄弟节点已处理(包括第一个)
+          siblings.forEach((sibling) => {
+            processedNodes.add(sibling.uuid || '');
+          });
+          
+          // 找到父节点
+          const parentNode = nodeMap.get(node.parentUuid);
+          if (parentNode && !allBranchNodeUuids.has(parentNode.uuid || '')) {
+            // 如果父节点存在且不是分支节点,显示父节点
+            // 所有子节点都显示在展开区域,不显示在主流程
+            console.log(`显示父节点: ${parentNode.uuid}, 所有子节点都放在展开区域:`, siblings.map(s => s.uuid));
+            mainFlowNodes.push({
+              ...parentNode,
+              isBranchParent: true,
+              branchNodes: siblings, // 所有子节点都显示在展开区域
+              branchParentUuid: node.parentUuid,
+            });
+          } else {
+            // 如果找不到父节点或父节点也是分支节点,使用第一个分支节点作为代表节点
+            // 但所有子节点(包括第一个)都显示在展开区域
+            const mainNodeUuid = firstSibling.uuid || '';
+            console.log(`使用第一个分支节点作为代表: ${mainNodeUuid}, 所有子节点都放在展开区域:`, siblings.map(s => s.uuid));
+            mainFlowNodes.push({
+              ...firstSibling,
+              isBranchParent: true,
+              branchNodes: siblings, // 所有子节点(包括第一个)都显示在展开区域
+              branchParentUuid: node.parentUuid,
+              mainNodeUuid: mainNodeUuid, // 记录主流程中显示的节点uuid
+            });
+          }
+        } else {
+          // 不是第一个节点,或者是已经处理过的分支组,直接跳过
+          console.log(`跳过分支节点: ${node.uuid} (不是第一个或已处理)`);
+        }
+        // 所有兄弟节点都不添加到主流程(它们会在展开时显示)
+      } else {
+        // 只有一个子节点,直接添加到主流程
+        if (!processedNodes.has(node.uuid || '')) {
+          processedNodes.add(node.uuid || '');
+          mainFlowNodes.push(node);
+        }
+      }
+    } else {
+      // 没有父节点(根节点),直接添加到主流程
+      if (!processedNodes.has(node.uuid || '')) {
+        processedNodes.add(node.uuid || '');
+        mainFlowNodes.push(node);
+      }
+    }
+  });
+
+  console.log('=== 最终主流程节点 ===');
+  mainFlowNodes.forEach((node, index) => {
+    console.log(`${index + 1}. ${node.nodeName} (${node.uuid})`, node.isBranchParent ? `[有分支: ${node.branchNodes?.length || 0}个]` : '');
+  });
+
+  // 切换分支展开/收起
+  const toggleBranchGroup = (parentUuid: string) => {
+    setExpandedBranchGroups((prev) => {
+      const newSet = new Set(prev);
+      if (newSet.has(parentUuid)) {
+        newSet.delete(parentUuid);
+      } else {
+        newSet.add(parentUuid);
+      }
+      return newSet;
+    });
+  };
+
+  // 获取 Timeline 的颜色
+  const getTimelineColor = (status: 'completed' | 'in_progress' | 'pending') => {
+    if (status === 'completed') return 'green';
+    if (status === 'in_progress') return 'blue';
+    return 'gray';
+  };
+
+  // 获取 Timeline 的图标
+  const getTimelineDot = (status: 'completed' | 'in_progress' | 'pending') => {
+    if (status === 'completed') {
+      return <CheckCircleOutlined style={{ fontSize: '16px', color: '#52c41a' }} />;
+    }
+    if (status === 'in_progress') {
+      return <SyncOutlined spin style={{ fontSize: '16px', color: '#1890ff' }} />;
+    }
+    return <ClockCircleOutlined style={{ fontSize: '16px', color: '#d9d9d9' }} />;
+  };
+
+  // 构建 Timeline 数据
+  const timelineItems = mainFlowNodes.map((node, index) => {
+    // 判断节点状态
+    let status: 'completed' | 'in_progress' | 'pending' = 'pending';
+    if (node.approvalStatus === 'approved') {
+      status = 'completed';
+    } else if (node.approvalStatus === 'unaudited' || node.approvalStatus === 'pending') {
+      if (index === 0) {
+        status = 'completed';
+      } else {
+        const prevNode = mainFlowNodes[index - 1];
+        if (prevNode && prevNode.approvalStatus === 'approved') {
+          status = 'in_progress';
+        } else {
+          status = 'pending';
+        }
+      }
+    }
+
+    const executorInfo = getExecutorInfo(node, node.type || '');
+    const time = node.createTime || node.updateTime;
+    const isBranchParent = node.isBranchParent && node.branchNodes && node.branchNodes.length > 1;
+    const branchParentUuid = (node as any).branchParentUuid || node.parentUuid || node.uuid || '';
+    const isExpanded = isBranchParent && expandedBranchGroups.has(branchParentUuid);
+    const nodeKey = node.uuid || String(node.id || index);
+    const isVisible = visibleNodes.has(nodeKey);
+
+    // 构建分支节点内容
+    const branchContent = isBranchParent && isExpanded && node.branchNodes ? (
+      <div className="mt-3 space-y-2" style={{ width: '100%' }}>
+        {node.branchNodes.map((branchNode, branchIndex) => {
+          let branchStatus: 'completed' | 'in_progress' | 'pending' = 'pending';
+          if (branchNode.approvalStatus === 'approved') {
+            branchStatus = 'completed';
+          } else if (branchNode.approvalStatus === 'unaudited' || branchNode.approvalStatus === 'pending') {
+            if (branchIndex > 0) {
+              const prevBranch = node.branchNodes![branchIndex - 1];
+              if (prevBranch && prevBranch.approvalStatus === 'approved') {
+                branchStatus = 'in_progress';
+              }
+            } else {
+              if (node.approvalStatus === 'approved') {
+                branchStatus = 'in_progress';
+              } else {
+                const hasCompletedBranch = node.branchNodes!.some((n, idx) => 
+                  idx < branchIndex && n.approvalStatus === 'approved'
+                );
+                if (hasCompletedBranch) {
+                  branchStatus = 'in_progress';
+                }
+              }
+            }
+          }
+
+          const branchExecutorInfo = getExecutorInfo(branchNode, branchNode.type || '');
+          const branchTime = branchNode.createTime || branchNode.updateTime;
+
+          return (
+            <div
+              key={branchNode.uuid || branchNode.id || branchIndex}
+              className={`mb-2 p-3 rounded-lg border-l-3 shadow-sm transition-all duration-300 hover:shadow bg-white ${
+                branchStatus === 'completed' 
+                  ? 'border-l-green-500 border-t border-r border-b border-gray-200' 
+                  : branchStatus === 'in_progress' 
+                  ? 'border-l-blue-500 border-t border-r border-b border-gray-200' 
+                  : 'border-l-gray-400 border-t border-r border-b border-gray-200'
+              }`}
+              style={{
+                animation: isVisible ? 'fadeInUp 0.5s ease-out' : 'none',
+                animationDelay: `${(index * 200 + branchIndex * 100) / 1000}s`,
+                maxWidth: '240px',
+              }}
+            >
+              <div className="flex items-start gap-2">
+                <div className="flex-shrink-0 mt-0.5">
+                  {getNodeIcon(branchNode.type || '', branchStatus)}
+                </div>
+                <div className="flex-1 min-w-0">
+                  <div className="flex items-center gap-1.5 mb-1.5">
+                    <span className={`font-semibold text-xs ${
+                      branchStatus === 'pending' ? 'text-gray-500' : 'text-gray-900'
+                    }`}>
+                      {branchNode.nodeName || '未知节点'}
+                    </span>
+                    <Tag 
+                      color={getTimelineColor(branchStatus)}
+                      className="text-xs"
+                    >
+                      {branchStatus === 'completed' ? '已完成' : branchStatus === 'in_progress' ? '进行中' : '待处理'}
+                    </Tag>
+                  </div>
+                  <div className={`text-xs ml-6 mb-1 ${
+                    branchStatus === 'pending' ? 'text-gray-400' : 'text-gray-600'
+                  }`}>
+                    {branchExecutorInfo}
+                  </div>
+                  {branchTime && (
+                    <div className="text-xs text-gray-500 ml-6 flex items-center">
+                      <Clock className="w-2.5 h-2.5 mr-1" />
+                      {formatDate(branchTime)}
+                    </div>
+                  )}
+                </div>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+    ) : null;
+
+    return {
+      key: nodeKey,
+      dot: getTimelineDot(status),
+      color: getTimelineColor(status),
+      children: (
+        <div 
+          className={`transition-all duration-500 ${
+            isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
+          }`}
+          style={{
+            animation: isVisible ? 'fadeInUp 0.5s ease-out' : 'none',
+            animationDelay: `${index * 200 / 1000}s`,
+          }}
+        >
+          <div 
+            className={`mb-3 p-4 rounded-lg border-l-4 shadow-sm transition-all duration-300 hover:shadow-md bg-white ${
+              status === 'completed' 
+                ? 'border-l-green-500 border-t border-r border-b border-gray-200' 
+                : status === 'in_progress' 
+                ? 'border-l-blue-500 border-t border-r border-b border-gray-200' 
+                : 'border-l-gray-400 border-t border-r border-b border-gray-200'
+            }`}
+            style={{ 
+              maxWidth: '100%', 
+              width: '100%',
+            }}
+          >
+            <div className="flex items-start justify-between gap-3">
+              <div className="flex-1 min-w-0">
+                <div className="flex items-center gap-2 mb-2">
+                  <div className="flex-shrink-0">
+                    {getNodeIcon(node.type || '', status)}
+                  </div>
+                  <span className={`font-semibold text-sm ${
+                    status === 'pending' ? 'text-gray-500' : 'text-gray-900'
+                  }`}>
+                    {node.nodeName || '未知节点'}
+                  </span>
+                  <Tag 
+                    color={getTimelineColor(status)}
+                    className="text-xs"
+                  >
+                    {status === 'completed' ? '已完成' : status === 'in_progress' ? '进行中' : '待处理'}
+                  </Tag>
+                </div>
+                <div className={`text-xs ml-7 mb-1.5 ${
+                  status === 'pending' ? 'text-gray-400' : 'text-gray-600'
+                }`}>
+                  {executorInfo}
+                </div>
+                {time && (
+                  <div className="text-xs text-gray-500 ml-7 flex items-center">
+                    <Clock className="w-3 h-3 mr-1" />
+                    {formatDate(time)}
+                  </div>
+                )}
+              </div>
+              
+              {/* 分支展开按钮 */}
+              {isBranchParent && (
+                <button
+                  onClick={() => toggleBranchGroup(branchParentUuid)}
+                  className="flex items-center justify-center w-6 h-6 rounded hover:bg-gray-100 transition-colors flex-shrink-0"
+                  title={isExpanded ? '收起分支' : '展开分支'}
+                >
+                  {isExpanded ? (
+                    <Minus className="w-3.5 h-3.5 text-gray-600" />
+                  ) : (
+                    <Plus className="w-3.5 h-3.5 text-gray-600" />
+                  )}
+                </button>
+              )}
+            </div>
+          </div>
+          {branchContent}
+        </div>
+      ),
+    };
+  });
+
+  return (
+    <div className="workflow-timeline">
+      <style>{`
+        @keyframes fadeInUp {
+          from {
+            opacity: 0;
+            transform: translateY(20px);
+          }
+          to {
+            opacity: 1;
+            transform: translateY(0);
+          }
+        }
+        .workflow-timeline .ant-timeline-item-content {
+          min-height: 60px;
+          max-width: 45%;
+        }
+        .workflow-timeline .ant-timeline-item-left .ant-timeline-item-content {
+          text-align: right;
+          padding-right: 24px;
+        }
+        .workflow-timeline .ant-timeline-item-left .ant-timeline-item-content > div {
+          display: inline-block;
+          text-align: left;
+          width: 100%;
+          max-width: 280px;
+        }
+        .workflow-timeline .ant-timeline-item-right .ant-timeline-item-content {
+          text-align: left;
+          padding-left: 24px;
+        }
+        .workflow-timeline .ant-timeline-item-right .ant-timeline-item-content > div {
+          display: inline-block;
+          text-align: left;
+          width: 100%;
+          max-width: 280px;
+        }
+        .workflow-timeline .ant-timeline-item-tail {
+          border-left: 2px solid #e8e8e8;
+        }
+        .workflow-timeline .ant-timeline-item-head {
+          width: 16px;
+          height: 16px;
+          border-width: 2px;
+        }
+        .workflow-timeline .ant-timeline-item-head-green {
+          border-color: #52c41a;
+          background-color: #52c41a;
+        }
+        .workflow-timeline .ant-timeline-item-head-blue {
+          border-color: #1890ff;
+          background-color: #1890ff;
+        }
+        .workflow-timeline .ant-timeline-item-head-gray {
+          border-color: #d9d9d9;
+          background-color: #fff;
+        }
+      `}</style>
+      <Timeline
+        mode="alternate"
+        items={timelineItems}
+        className="custom-timeline"
+      />
+    </div>
+  );
+};
+
 export default function WorkJobDetail() {
   const navigate = useNavigate();
   const [searchParams] = useSearchParams();
@@ -173,6 +656,9 @@ export default function WorkJobDetail() {
   // 流程树状态
   const [workflowTree, setWorkflowTree] = useState<WorkflowNode[]>([]);
   const [expandedBranches, setExpandedBranches] = useState<Set<string>>(new Set());
+  
+  // 展开的分支组(按 parentUuid)
+  const [expandedBranchGroups, setExpandedBranchGroups] = useState<Set<string>>(new Set());
 
   // 获取作业详情
   useEffect(() => {
@@ -564,7 +1050,19 @@ export default function WorkJobDetail() {
               </h2>
             </div>
             <div className="flex-1 overflow-y-auto p-6">
-              {/* 作业流程渲染 - 待重新设计 */}
+              {/* 作业流程渲染 */}
+              {jobDetail?.workflowWorkNodeDOList && Array.isArray(jobDetail.workflowWorkNodeDOList) && jobDetail.workflowWorkNodeDOList.length > 0 ? (
+                <WorkflowRenderer 
+                  nodeList={jobDetail.workflowWorkNodeDOList}
+                  expandedBranchGroups={expandedBranchGroups}
+                  setExpandedBranchGroups={setExpandedBranchGroups}
+                  getExecutorInfo={getExecutorInfo}
+                  getNodeIcon={getNodeIcon}
+                  formatDate={formatDateWithFormat}
+                />
+              ) : (
+                <div className="text-center text-gray-400 py-8">暂无流程数据</div>
+              )}
             </div>
           </div>