|
@@ -1,4 +1,4 @@
|
|
|
-import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
|
import {
|
|
import {
|
|
|
User,
|
|
User,
|
|
@@ -11,23 +11,12 @@ import {
|
|
|
Star,
|
|
Star,
|
|
|
ArrowLeft,
|
|
ArrowLeft,
|
|
|
Clock,
|
|
Clock,
|
|
|
- List
|
|
|
|
|
|
|
+ List,
|
|
|
|
|
+ Plus,
|
|
|
|
|
+ Minus,
|
|
|
|
|
+ Loader2
|
|
|
} from 'lucide-react';
|
|
} from 'lucide-react';
|
|
|
-import ReactFlow, {
|
|
|
|
|
- Node,
|
|
|
|
|
- Edge,
|
|
|
|
|
- useNodesState,
|
|
|
|
|
- useEdgesState,
|
|
|
|
|
- NodeTypes,
|
|
|
|
|
- ConnectionMode,
|
|
|
|
|
- Handle,
|
|
|
|
|
- Position,
|
|
|
|
|
- BaseEdge,
|
|
|
|
|
- EdgeTypes,
|
|
|
|
|
- getStraightPath,
|
|
|
|
|
-} from 'reactflow';
|
|
|
|
|
-import 'reactflow/dist/style.css';
|
|
|
|
|
-import { workJobApi, WorkJobVO } from '../api/WorkJob';
|
|
|
|
|
|
|
+import { workJobApi, WorkJobVO, WorkflowWorkNodeDO } from '../api/WorkJob';
|
|
|
import { formatDateWithFormat } from '../utils/formatTime';
|
|
import { formatDateWithFormat } from '../utils/formatTime';
|
|
|
|
|
|
|
|
interface WorkflowStep {
|
|
interface WorkflowStep {
|
|
@@ -54,134 +43,124 @@ interface FlowRecord {
|
|
|
duration?: string; // 耗时
|
|
duration?: string; // 耗时
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 使用 ReactFlow 默认的连线效果,不需要自定义边组件
|
|
|
|
|
-
|
|
|
|
|
-// 节点类型映射(简化版,只读模式)
|
|
|
|
|
-const nodeTypes: NodeTypes = {
|
|
|
|
|
- createJob: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
|
|
|
|
|
- confirm: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
|
|
|
|
|
- review: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
|
|
|
|
|
- inputInfo: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
|
|
|
|
|
- isolation: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
|
|
|
|
|
- releaseIsolation: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
|
|
|
|
|
- returnLock: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
|
|
|
|
|
- complete: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
|
|
|
|
|
|
|
+// 流程节点数据结构
|
|
|
|
|
+interface WorkflowNode {
|
|
|
|
|
+ uuid: string;
|
|
|
|
|
+ nodeName: string;
|
|
|
|
|
+ type: string;
|
|
|
|
|
+ status: 'completed' | 'in_progress' | 'pending';
|
|
|
|
|
+ workNode: WorkflowWorkNodeDO;
|
|
|
|
|
+ children: WorkflowNode[];
|
|
|
|
|
+ isBranch: boolean; // 是否为分支节点(有多个子节点)
|
|
|
|
|
+ branchNodes?: WorkflowNode[]; // 分支节点列表
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取节点图标
|
|
|
|
|
+const getNodeIcon = (type: string, status: 'completed' | 'in_progress' | 'pending') => {
|
|
|
|
|
+ const iconClass = status === 'completed'
|
|
|
|
|
+ ? 'text-green-600'
|
|
|
|
|
+ : status === 'in_progress'
|
|
|
|
|
+ ? 'text-blue-600'
|
|
|
|
|
+ : 'text-gray-400';
|
|
|
|
|
+
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case 'createJob':
|
|
|
|
|
+ return <CheckCircle2 className={`w-5 h-5 ${iconClass}`} />;
|
|
|
|
|
+ case 'review':
|
|
|
|
|
+ case 'confirm':
|
|
|
|
|
+ return <Settings className={`w-5 h-5 ${iconClass}`} />;
|
|
|
|
|
+ case 'inputInfo':
|
|
|
|
|
+ return <FileText className={`w-5 h-5 ${iconClass}`} />;
|
|
|
|
|
+ case 'isolation':
|
|
|
|
|
+ return <Shield className={`w-5 h-5 ${iconClass}`} />;
|
|
|
|
|
+ case 'releaseIsolation':
|
|
|
|
|
+ case 'returnLock':
|
|
|
|
|
+ return <Lock className={`w-5 h-5 ${iconClass}`} />;
|
|
|
|
|
+ case 'complete':
|
|
|
|
|
+ return <Flag className={`w-5 h-5 ${iconClass}`} />;
|
|
|
|
|
+ default:
|
|
|
|
|
+ return <FileText className={`w-5 h-5 ${iconClass}`} />;
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// 简化的自定义节点组件(只读模式)
|
|
|
|
|
-function CustomNode({ data, selected, id }: any) {
|
|
|
|
|
- const getNodeIcon = () => {
|
|
|
|
|
- switch (data?.type) {
|
|
|
|
|
- case 'createJob':
|
|
|
|
|
- return <CheckCircle2 className="w-6 h-6" />;
|
|
|
|
|
- case 'review':
|
|
|
|
|
- case 'confirm':
|
|
|
|
|
- return <Settings className="w-6 h-6" />;
|
|
|
|
|
- case 'inputInfo':
|
|
|
|
|
- return <FileText className="w-6 h-6" />;
|
|
|
|
|
- case 'isolation':
|
|
|
|
|
- return <Shield className="w-6 h-6" />;
|
|
|
|
|
- case 'releaseIsolation':
|
|
|
|
|
- case 'returnLock':
|
|
|
|
|
- return <Lock className="w-6 h-6" />;
|
|
|
|
|
- case 'complete':
|
|
|
|
|
- return <Flag className="w-6 h-6" />;
|
|
|
|
|
- default:
|
|
|
|
|
- return <FileText className="w-6 h-6" />;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
|
|
+// 获取状态图标(圆形指示器)
|
|
|
|
|
+const getStatusIcon = (status: 'completed' | 'in_progress' | 'pending') => {
|
|
|
|
|
+ if (status === 'completed') {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="w-6 h-6 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
+ <CheckCircle2 className="w-4 h-4 text-white" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ } else if (status === 'in_progress') {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
+ <Loader2 className="w-4 h-4 text-white animate-spin" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="w-6 h-6 rounded-full border-2 border-gray-300 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
+ <div className="w-2 h-2 rounded-full bg-gray-300" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- // 根据状态确定节点颜色
|
|
|
|
|
- const getStatusColor = () => {
|
|
|
|
|
- if (data?.status === 'completed') {
|
|
|
|
|
- return { icon: 'text-green-600' };
|
|
|
|
|
- } else if (data?.status === 'in_progress') {
|
|
|
|
|
- return { icon: 'text-blue-600' };
|
|
|
|
|
- } else {
|
|
|
|
|
- return { icon: 'text-gray-400' };
|
|
|
|
|
|
|
+// 获取执行人信息
|
|
|
|
|
+const getExecutorInfo = (workNode: WorkflowWorkNodeDO, type: string): string => {
|
|
|
|
|
+ if (!workNode) return '待处理';
|
|
|
|
|
+
|
|
|
|
|
+ let executorName = '';
|
|
|
|
|
+ if (workNode.nodeUserList && Array.isArray(workNode.nodeUserList)) {
|
|
|
|
|
+ const workerUser = workNode.nodeUserList.find((user: any) => user.type === 'worker' || !user.type);
|
|
|
|
|
+ if (workerUser && workerUser.userName) {
|
|
|
|
|
+ executorName = workerUser.userName;
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // 获取人员信息
|
|
|
|
|
- const getExecutorInfo = () => {
|
|
|
|
|
- const workNode = data?.workNode;
|
|
|
|
|
- if (!workNode) return '待处理';
|
|
|
|
|
-
|
|
|
|
|
- // 尝试从多个字段获取执行人名称
|
|
|
|
|
- // 优先从 nodeUserList 中查找 worker 类型的用户
|
|
|
|
|
- let executorName = '';
|
|
|
|
|
- if (workNode.nodeUserList && Array.isArray(workNode.nodeUserList)) {
|
|
|
|
|
- const workerUser = workNode.nodeUserList.find((user: any) => user.type === 'worker' || !user.type);
|
|
|
|
|
- if (workerUser && workerUser.userName) {
|
|
|
|
|
- executorName = workerUser.userName;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!executorName) {
|
|
|
|
|
+ if (workNode.workerUserName) {
|
|
|
|
|
+ executorName = workNode.workerUserName;
|
|
|
|
|
+ } else if (workNode.initiatorName) {
|
|
|
|
|
+ executorName = workNode.initiatorName;
|
|
|
|
|
+ } else if (workNode.workerUserId) {
|
|
|
|
|
+ executorName = String(workNode.workerUserId);
|
|
|
|
|
+ } else if (workNode.initiator) {
|
|
|
|
|
+ executorName = workNode.initiator;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 如果没有从 nodeUserList 找到,尝试其他字段
|
|
|
|
|
- if (!executorName) {
|
|
|
|
|
- if (workNode.workerUserName) {
|
|
|
|
|
- executorName = workNode.workerUserName;
|
|
|
|
|
- } else if (workNode.initiatorName) {
|
|
|
|
|
- executorName = workNode.initiatorName;
|
|
|
|
|
- } else if (workNode.workerUserId) {
|
|
|
|
|
- executorName = String(workNode.workerUserId);
|
|
|
|
|
- } else if (workNode.initiator) {
|
|
|
|
|
- executorName = workNode.initiator;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!executorName) return '待处理';
|
|
|
|
|
+
|
|
|
|
|
+ if (type === 'createJob') {
|
|
|
|
|
+ return `发起人: ${executorName}`;
|
|
|
|
|
+ } else if (type === 'review' || type === 'confirm') {
|
|
|
|
|
+ return `审核人: ${executorName}`;
|
|
|
|
|
+ } else if (type === 'isolation' || type === 'releaseIsolation' || type === 'returnLock') {
|
|
|
|
|
+ return `操作人: ${executorName}`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return `执行人: ${executorName}`;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取节点状态
|
|
|
|
|
+const getNodeStatus = (workNode: WorkflowWorkNodeDO, nodeMap: Map<string, WorkflowWorkNodeDO>): 'completed' | 'in_progress' | 'pending' => {
|
|
|
|
|
+ if (!workNode) return 'pending';
|
|
|
|
|
+
|
|
|
|
|
+ if (workNode.approvalStatus === 'approved') {
|
|
|
|
|
+ return 'completed';
|
|
|
|
|
+ } else if (workNode.approvalStatus === 'unaudited' || workNode.approvalStatus === 'pending') {
|
|
|
|
|
+ if (!workNode.parentUuid || workNode.parentUuid === '') {
|
|
|
|
|
+ return 'completed';
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- if (!executorName) return '待处理';
|
|
|
|
|
-
|
|
|
|
|
- if (data?.type === 'createJob') {
|
|
|
|
|
- return `发起人: ${executorName}`;
|
|
|
|
|
- } else if (data?.type === 'review' || data?.type === 'confirm') {
|
|
|
|
|
- return `审核人: ${executorName}`;
|
|
|
|
|
- } else if (data?.type === 'isolation' || data?.type === 'releaseIsolation' || data?.type === 'returnLock') {
|
|
|
|
|
- return `操作人: ${executorName}`;
|
|
|
|
|
- } else {
|
|
|
|
|
- return `执行人: ${executorName}`;
|
|
|
|
|
|
|
+ const parentNode = nodeMap.get(workNode.parentUuid);
|
|
|
|
|
+ if (parentNode && parentNode.approvalStatus === 'approved') {
|
|
|
|
|
+ return 'in_progress';
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const statusColor = getStatusColor();
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="flex items-center gap-3 py-2 relative">
|
|
|
|
|
- {/* 连接点 - 隐藏但保留用于连接 */}
|
|
|
|
|
- <Handle
|
|
|
|
|
- id="top-target"
|
|
|
|
|
- type="target"
|
|
|
|
|
- position={Position.Top}
|
|
|
|
|
- className="!w-0 !h-0 !border-0 !bg-transparent !opacity-0 !pointer-events-none"
|
|
|
|
|
- style={{ top: -6, left: '50%', transform: 'translateX(-50%)', visibility: 'hidden' }}
|
|
|
|
|
- isConnectable={false}
|
|
|
|
|
- />
|
|
|
|
|
- <Handle
|
|
|
|
|
- id="bottom-source"
|
|
|
|
|
- type="source"
|
|
|
|
|
- position={Position.Bottom}
|
|
|
|
|
- className="!w-0 !h-0 !border-0 !bg-transparent !opacity-0 !pointer-events-none"
|
|
|
|
|
- style={{ bottom: -6, left: '50%', transform: 'translateX(-50%)', visibility: 'hidden' }}
|
|
|
|
|
- isConnectable={false}
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
- {/* 图标 */}
|
|
|
|
|
- <div className={`${statusColor.icon} flex items-center justify-center flex-shrink-0`}>
|
|
|
|
|
- {getNodeIcon()}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 节点名称和人员信息 */}
|
|
|
|
|
- <div className="flex flex-col gap-1">
|
|
|
|
|
- <div className="font-semibold text-base text-gray-900">
|
|
|
|
|
- {data?.label || data?.nodeName || '节点'}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="text-sm text-gray-600">
|
|
|
|
|
- {getExecutorInfo()}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ return 'pending';
|
|
|
|
|
+ }
|
|
|
|
|
+ return 'pending';
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
export default function WorkJobDetail() {
|
|
export default function WorkJobDetail() {
|
|
|
const navigate = useNavigate();
|
|
const navigate = useNavigate();
|
|
@@ -191,11 +170,9 @@ export default function WorkJobDetail() {
|
|
|
const [jobDetail, setJobDetail] = useState<WorkJobVO | null>(null);
|
|
const [jobDetail, setJobDetail] = useState<WorkJobVO | null>(null);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
- // ReactFlow 状态
|
|
|
|
|
- const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
|
|
|
- const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
|
|
|
- const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
|
|
|
- const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);
|
|
|
|
|
|
|
+ // 流程树状态
|
|
|
|
|
+ const [workflowTree, setWorkflowTree] = useState<WorkflowNode[]>([]);
|
|
|
|
|
+ const [expandedBranches, setExpandedBranches] = useState<Set<string>>(new Set());
|
|
|
|
|
|
|
|
// 获取作业详情
|
|
// 获取作业详情
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -214,9 +191,13 @@ export default function WorkJobDetail() {
|
|
|
setJobDetail(data);
|
|
setJobDetail(data);
|
|
|
console.log('作业详情数据:', data);
|
|
console.log('作业详情数据:', data);
|
|
|
|
|
|
|
|
- // 加载完成后,解析 designContent 并渲染流程
|
|
|
|
|
- if (data?.designContent) {
|
|
|
|
|
- loadWorkflowFromDesignContent(data);
|
|
|
|
|
|
|
+ // 构建流程树
|
|
|
|
|
+ if (data?.workflowWorkNodeDOList && Array.isArray(data.workflowWorkNodeDOList) && data.workflowWorkNodeDOList.length > 0) {
|
|
|
|
|
+ console.log('准备构建流程树,节点数量:', data.workflowWorkNodeDOList.length);
|
|
|
|
|
+ buildWorkflowTree(data.workflowWorkNodeDOList);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('没有流程节点数据,workflowWorkNodeDOList:', data?.workflowWorkNodeDOList);
|
|
|
|
|
+ setWorkflowTree([]);
|
|
|
}
|
|
}
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
console.error('获取作业详情失败:', error);
|
|
console.error('获取作业详情失败:', error);
|
|
@@ -225,150 +206,100 @@ export default function WorkJobDetail() {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 从 designContent 加载流程并渲染到 ReactFlow
|
|
|
|
|
- const loadWorkflowFromDesignContent = (jobData: WorkJobVO) => {
|
|
|
|
|
- try {
|
|
|
|
|
- // 解析 designContent
|
|
|
|
|
- let designContentData: any = null;
|
|
|
|
|
- if (jobData.designContent) {
|
|
|
|
|
- try {
|
|
|
|
|
- designContentData = typeof jobData.designContent === 'string'
|
|
|
|
|
- ? JSON.parse(jobData.designContent)
|
|
|
|
|
- : jobData.designContent;
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.warn('解析 designContent 失败:', e);
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ // 构建流程树
|
|
|
|
|
+ const buildWorkflowTree = (nodeList: WorkflowWorkNodeDO[]) => {
|
|
|
|
|
+ console.log('开始构建流程树,节点列表:', nodeList);
|
|
|
|
|
+
|
|
|
|
|
+ if (!nodeList || nodeList.length === 0) {
|
|
|
|
|
+ console.warn('节点列表为空');
|
|
|
|
|
+ setWorkflowTree([]);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建节点映射
|
|
|
|
|
+ const nodeMap = new Map<string, WorkflowWorkNodeDO>();
|
|
|
|
|
+ nodeList.forEach((node) => {
|
|
|
|
|
+ if (node.uuid) {
|
|
|
|
|
+ nodeMap.set(node.uuid, node);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 构建父子关系映射
|
|
|
|
|
+ const childrenMap = new Map<string, WorkflowWorkNodeDO[]>();
|
|
|
|
|
+ nodeList.forEach((node) => {
|
|
|
|
|
+ if (node.parentUuid) {
|
|
|
|
|
+ if (!childrenMap.has(node.parentUuid)) {
|
|
|
|
|
+ childrenMap.set(node.parentUuid, []);
|
|
|
}
|
|
}
|
|
|
|
|
+ childrenMap.get(node.parentUuid)!.push(node);
|
|
|
}
|
|
}
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log('子节点映射:', Array.from(childrenMap.entries()));
|
|
|
|
|
+
|
|
|
|
|
+ // 找到根节点(parentUuid 为空或不在 nodeList 中的节点)
|
|
|
|
|
+ const rootNodes = nodeList.filter((node) => !node.parentUuid || node.parentUuid === '');
|
|
|
|
|
+ console.log('根节点:', rootNodes);
|
|
|
|
|
|
|
|
- if (!designContentData || !designContentData.nodes || !Array.isArray(designContentData.nodes)) {
|
|
|
|
|
- console.warn('designContent 数据格式不正确');
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ // 递归构建节点树
|
|
|
|
|
+ const buildNode = (workNode: WorkflowWorkNodeDO): WorkflowNode => {
|
|
|
|
|
+ const status = getNodeStatus(workNode, nodeMap);
|
|
|
|
|
+ const children = childrenMap.get(workNode.uuid || '') || [];
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否有分支(多个子节点)
|
|
|
|
|
+ const isBranch = children.length > 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 构建子节点
|
|
|
|
|
+ const childNodes = children.map((child) => buildNode(child));
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有分支,第一个子节点作为主流程,其他作为分支
|
|
|
|
|
+ let mainChild: WorkflowNode | null = null;
|
|
|
|
|
+ let branchNodes: WorkflowNode[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ if (isBranch && childNodes.length > 0) {
|
|
|
|
|
+ mainChild = childNodes[0];
|
|
|
|
|
+ branchNodes = childNodes.slice(1);
|
|
|
|
|
+ } else if (childNodes.length === 1) {
|
|
|
|
|
+ mainChild = childNodes[0];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 构建节点映射(通过 uuid)
|
|
|
|
|
- const nodeMap = new Map<string, any>();
|
|
|
|
|
- if (jobData.workflowWorkNodeDOList && Array.isArray(jobData.workflowWorkNodeDOList)) {
|
|
|
|
|
- jobData.workflowWorkNodeDOList.forEach((node: any) => {
|
|
|
|
|
- if (node.uuid) {
|
|
|
|
|
- nodeMap.set(node.uuid, node);
|
|
|
|
|
- }
|
|
|
|
|
- } );
|
|
|
|
|
-}
|
|
|
|
|
- const getNodeStatus = (nodeUuid: string): 'completed' | 'in_progress' | 'pending' => {
|
|
|
|
|
- const workNode = nodeMap.get(nodeUuid);
|
|
|
|
|
- if (!workNode) return 'pending';
|
|
|
|
|
-
|
|
|
|
|
- if (workNode.approvalStatus === 'approved') {
|
|
|
|
|
- return 'completed';
|
|
|
|
|
- } else if (workNode.approvalStatus === 'unaudited' || workNode.approvalStatus === 'pending') {
|
|
|
|
|
- // 检查父节点是否已完成
|
|
|
|
|
- if (!workNode.parentUuid || workNode.parentUuid === '') {
|
|
|
|
|
- return 'completed'; // 根节点
|
|
|
|
|
- }
|
|
|
|
|
- const parentNode = nodeMap.get(workNode.parentUuid);
|
|
|
|
|
- if (parentNode && parentNode.approvalStatus === 'approved') {
|
|
|
|
|
- return 'in_progress';
|
|
|
|
|
- }
|
|
|
|
|
- return 'pending';
|
|
|
|
|
- }
|
|
|
|
|
- return 'pending';
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ uuid: workNode.uuid || '',
|
|
|
|
|
+ nodeName: workNode.nodeName || '未知节点',
|
|
|
|
|
+ type: workNode.type || 'createJob',
|
|
|
|
|
+ status,
|
|
|
|
|
+ workNode,
|
|
|
|
|
+ children: mainChild ? [mainChild] : [],
|
|
|
|
|
+ isBranch,
|
|
|
|
|
+ branchNodes: branchNodes.length > 0 ? branchNodes : undefined,
|
|
|
};
|
|
};
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // 转换为 ReactFlow 的 nodes
|
|
|
|
|
- // 将横向布局转换为纵向布局:交换 x 和 y 坐标
|
|
|
|
|
- const importedNodes: Node[] = designContentData.nodes.map((node: any) => {
|
|
|
|
|
- const nodeId = node.id || node.uuid;
|
|
|
|
|
- const nodeData = node.data || {};
|
|
|
|
|
- const workNode = nodeMap.get(nodeId);
|
|
|
|
|
- const status = getNodeStatus(nodeId);
|
|
|
|
|
-
|
|
|
|
|
- // 获取原始位置
|
|
|
|
|
- const originalPosition = node.position || { x: 0, y: 0 };
|
|
|
|
|
-
|
|
|
|
|
- // 交换 x 和 y 坐标,实现横向到纵向的转换
|
|
|
|
|
- // 同时应用缩放因子,使节点间距更紧凑(0.6 表示缩小到原来的 60%)
|
|
|
|
|
- const spacingScale = 0.6;
|
|
|
|
|
- const verticalPosition = {
|
|
|
|
|
- x: originalPosition.y * spacingScale,
|
|
|
|
|
- y: originalPosition.x * spacingScale,
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- id: nodeId,
|
|
|
|
|
- type: node.type || 'createJob',
|
|
|
|
|
- position: verticalPosition,
|
|
|
|
|
- data: {
|
|
|
|
|
- ...nodeData,
|
|
|
|
|
- label: nodeData.label || node.label || node.nodeName || workNode?.nodeName || '节点',
|
|
|
|
|
- type: node.type || nodeData.type || 'createJob',
|
|
|
|
|
- status: status,
|
|
|
|
|
- workNode: workNode, // 传递 workNode 以便获取人员信息
|
|
|
|
|
- },
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 转换为 ReactFlow 的 edges
|
|
|
|
|
- // 统一使用垂直布局:所有连接线都从节点底部垂直向下
|
|
|
|
|
- const importedEdges: Edge[] = (designContentData.edges || []).map((edge: any) => {
|
|
|
|
|
- // 检查节点位置,确保 source 在上,target 在下
|
|
|
|
|
- const sourceNode = importedNodes.find((n: Node) => n.id === edge.source);
|
|
|
|
|
- const targetNode = importedNodes.find((n: Node) => n.id === edge.target);
|
|
|
|
|
-
|
|
|
|
|
- // 如果 target 在 source 上方,交换它们(确保连接线从上到下)
|
|
|
|
|
- let finalSource = edge.source;
|
|
|
|
|
- let finalTarget = edge.target;
|
|
|
|
|
- if (sourceNode && targetNode && targetNode.position.y < sourceNode.position.y) {
|
|
|
|
|
- finalSource = edge.target;
|
|
|
|
|
- finalTarget = edge.source;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 使用最终确定的 source 节点状态来确定边的颜色
|
|
|
|
|
- const sourceStatus = getNodeStatus(finalSource);
|
|
|
|
|
-
|
|
|
|
|
- // 根据状态确定边的颜色
|
|
|
|
|
- let edgeColor = '#d1d5db'; // 默认灰色
|
|
|
|
|
- let edgeStyle = 'dashed';
|
|
|
|
|
- if (sourceStatus === 'completed') {
|
|
|
|
|
- edgeColor = '#10b981'; // 绿色
|
|
|
|
|
- edgeStyle = 'solid';
|
|
|
|
|
- } else if (sourceStatus === 'in_progress') {
|
|
|
|
|
- edgeColor = '#3b82f6'; // 蓝色
|
|
|
|
|
- edgeStyle = 'solid';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- id: edge.id || `${finalSource}-${finalTarget}`,
|
|
|
|
|
- source: finalSource,
|
|
|
|
|
- target: finalTarget,
|
|
|
|
|
- sourceHandle: 'bottom-source', // 统一使用底部
|
|
|
|
|
- targetHandle: 'top-target', // 统一使用顶部
|
|
|
|
|
- // 不指定 type,使用 ReactFlow 默认的连线效果
|
|
|
|
|
- style: {
|
|
|
|
|
- strokeWidth: 4,
|
|
|
|
|
- stroke: edgeColor,
|
|
|
|
|
- strokeDasharray: edgeStyle === 'dashed' ? '5,5' : undefined,
|
|
|
|
|
- },
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- console.log('导入的节点:', importedNodes);
|
|
|
|
|
- console.log('导入的连线:', importedEdges);
|
|
|
|
|
-
|
|
|
|
|
- setNodes(importedNodes);
|
|
|
|
|
- setEdges(importedEdges);
|
|
|
|
|
-
|
|
|
|
|
- // 自动适应视图
|
|
|
|
|
- if (reactFlowInstance && importedNodes.length > 0) {
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- reactFlowInstance.fitView({ padding: 0.2, duration: 300 });
|
|
|
|
|
- }, 100);
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('加载流程失败:', error);
|
|
|
|
|
|
|
+ // 构建主流程树(从第一个根节点开始)
|
|
|
|
|
+ if (rootNodes.length > 0) {
|
|
|
|
|
+ const rootNode = rootNodes[0];
|
|
|
|
|
+ const tree = [buildNode(rootNode)];
|
|
|
|
|
+ console.log('构建完成的流程树:', tree);
|
|
|
|
|
+ setWorkflowTree(tree);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('未找到根节点');
|
|
|
|
|
+ setWorkflowTree([]);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 切换分支展开/收起
|
|
|
|
|
+ const toggleBranch = (nodeUuid: string) => {
|
|
|
|
|
+ setExpandedBranches((prev) => {
|
|
|
|
|
+ const newSet = new Set(prev);
|
|
|
|
|
+ if (newSet.has(nodeUuid)) {
|
|
|
|
|
+ newSet.delete(nodeUuid);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ newSet.add(nodeUuid);
|
|
|
|
|
+ }
|
|
|
|
|
+ return newSet;
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
// 格式化状态文本
|
|
// 格式化状态文本
|
|
|
const getStatusText = (status: string | number | undefined): string => {
|
|
const getStatusText = (status: string | number | undefined): string => {
|
|
|
const statusMap: Record<string, string> = {
|
|
const statusMap: Record<string, string> = {
|
|
@@ -633,38 +564,7 @@ export default function WorkJobDetail() {
|
|
|
</h2>
|
|
</h2>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
|
- {/* 作业流程渲染 - 使用 ReactFlow */}
|
|
|
|
|
- {(() => {
|
|
|
|
|
- if (!jobDetail?.designContent || nodes.length === 0) {
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="flex items-center justify-center h-full text-gray-400">
|
|
|
|
|
- {loading ? '加载中...' : '暂无流程数据'}
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <div className="relative" style={{ minHeight: '100%', height: '600px' }} ref={reactFlowWrapper}>
|
|
|
|
|
- <ReactFlow
|
|
|
|
|
- nodes={nodes}
|
|
|
|
|
- edges={edges}
|
|
|
|
|
- onNodesChange={onNodesChange}
|
|
|
|
|
- onEdgesChange={onEdgesChange}
|
|
|
|
|
- nodeTypes={nodeTypes}
|
|
|
|
|
- fitView
|
|
|
|
|
- onInit={setReactFlowInstance}
|
|
|
|
|
- nodesDraggable={false}
|
|
|
|
|
- nodesConnectable={false}
|
|
|
|
|
- elementsSelectable={false}
|
|
|
|
|
- connectionMode={ConnectionMode.Loose}
|
|
|
|
|
- defaultEdgeOptions={{
|
|
|
|
|
- style: { strokeWidth: 4 },
|
|
|
|
|
- }}
|
|
|
|
|
- >
|
|
|
|
|
- </ReactFlow>
|
|
|
|
|
- </div>
|
|
|
|
|
- );
|
|
|
|
|
- })()}
|
|
|
|
|
|
|
+ {/* 作业流程渲染 - 待重新设计 */}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|