WorkJobDetail.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { useNavigate, useSearchParams } from 'react-router-dom';
  3. import {
  4. User,
  5. CheckCircle2,
  6. Settings,
  7. FileText,
  8. Shield,
  9. Lock,
  10. Flag,
  11. Star,
  12. ArrowLeft,
  13. Clock,
  14. List
  15. } from 'lucide-react';
  16. import ReactFlow, {
  17. Node,
  18. Edge,
  19. useNodesState,
  20. useEdgesState,
  21. NodeTypes,
  22. ConnectionMode,
  23. Handle,
  24. Position,
  25. BaseEdge,
  26. EdgeTypes,
  27. getStraightPath,
  28. } from 'reactflow';
  29. import 'reactflow/dist/style.css';
  30. import { workJobApi, WorkJobVO } from '../api/WorkJob';
  31. import { formatDateWithFormat } from '../utils/formatTime';
  32. interface WorkflowStep {
  33. id: string;
  34. type: string;
  35. title: string;
  36. assignee?: string;
  37. assigneeName?: string;
  38. time?: string;
  39. status: 'completed' | 'in_progress' | 'pending';
  40. icon: React.ReactNode;
  41. hasDetail?: boolean;
  42. }
  43. // 流转记录数据类型
  44. interface FlowRecord {
  45. id: string;
  46. taskNode: string; // 任务节点
  47. executor: string; // 执行人
  48. startTime?: string; // 开始时间
  49. endTime?: string; // 结束时间
  50. taskStatus: 'completed' | 'in_progress' | 'pending'; // 任务状态
  51. executionDescription?: string; // 执行说明
  52. duration?: string; // 耗时
  53. }
  54. // 使用 ReactFlow 默认的连线效果,不需要自定义边组件
  55. // 节点类型映射(简化版,只读模式)
  56. const nodeTypes: NodeTypes = {
  57. createJob: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
  58. confirm: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
  59. review: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
  60. inputInfo: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
  61. isolation: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
  62. releaseIsolation: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
  63. returnLock: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
  64. complete: ({ data, selected }: any) => <CustomNode data={data} selected={selected} />,
  65. };
  66. // 简化的自定义节点组件(只读模式)
  67. function CustomNode({ data, selected, id }: any) {
  68. const getNodeIcon = () => {
  69. switch (data?.type) {
  70. case 'createJob':
  71. return <CheckCircle2 className="w-6 h-6" />;
  72. case 'review':
  73. case 'confirm':
  74. return <Settings className="w-6 h-6" />;
  75. case 'inputInfo':
  76. return <FileText className="w-6 h-6" />;
  77. case 'isolation':
  78. return <Shield className="w-6 h-6" />;
  79. case 'releaseIsolation':
  80. case 'returnLock':
  81. return <Lock className="w-6 h-6" />;
  82. case 'complete':
  83. return <Flag className="w-6 h-6" />;
  84. default:
  85. return <FileText className="w-6 h-6" />;
  86. }
  87. };
  88. // 根据状态确定节点颜色
  89. const getStatusColor = () => {
  90. if (data?.status === 'completed') {
  91. return { icon: 'text-green-600' };
  92. } else if (data?.status === 'in_progress') {
  93. return { icon: 'text-blue-600' };
  94. } else {
  95. return { icon: 'text-gray-400' };
  96. }
  97. };
  98. // 获取人员信息
  99. const getExecutorInfo = () => {
  100. const workNode = data?.workNode;
  101. if (!workNode) return '待处理';
  102. // 尝试从多个字段获取执行人名称
  103. // 优先从 nodeUserList 中查找 worker 类型的用户
  104. let executorName = '';
  105. if (workNode.nodeUserList && Array.isArray(workNode.nodeUserList)) {
  106. const workerUser = workNode.nodeUserList.find((user: any) => user.type === 'worker' || !user.type);
  107. if (workerUser && workerUser.userName) {
  108. executorName = workerUser.userName;
  109. }
  110. }
  111. // 如果没有从 nodeUserList 找到,尝试其他字段
  112. if (!executorName) {
  113. if (workNode.workerUserName) {
  114. executorName = workNode.workerUserName;
  115. } else if (workNode.initiatorName) {
  116. executorName = workNode.initiatorName;
  117. } else if (workNode.workerUserId) {
  118. executorName = String(workNode.workerUserId);
  119. } else if (workNode.initiator) {
  120. executorName = workNode.initiator;
  121. }
  122. }
  123. if (!executorName) return '待处理';
  124. if (data?.type === 'createJob') {
  125. return `发起人: ${executorName}`;
  126. } else if (data?.type === 'review' || data?.type === 'confirm') {
  127. return `审核人: ${executorName}`;
  128. } else if (data?.type === 'isolation' || data?.type === 'releaseIsolation' || data?.type === 'returnLock') {
  129. return `操作人: ${executorName}`;
  130. } else {
  131. return `执行人: ${executorName}`;
  132. }
  133. };
  134. const statusColor = getStatusColor();
  135. return (
  136. <div className="flex items-center gap-3 py-2 relative">
  137. {/* 连接点 - 隐藏但保留用于连接 */}
  138. <Handle
  139. id="top-target"
  140. type="target"
  141. position={Position.Top}
  142. className="!w-0 !h-0 !border-0 !bg-transparent !opacity-0 !pointer-events-none"
  143. style={{ top: -6, left: '50%', transform: 'translateX(-50%)', visibility: 'hidden' }}
  144. isConnectable={false}
  145. />
  146. <Handle
  147. id="bottom-source"
  148. type="source"
  149. position={Position.Bottom}
  150. className="!w-0 !h-0 !border-0 !bg-transparent !opacity-0 !pointer-events-none"
  151. style={{ bottom: -6, left: '50%', transform: 'translateX(-50%)', visibility: 'hidden' }}
  152. isConnectable={false}
  153. />
  154. {/* 图标 */}
  155. <div className={`${statusColor.icon} flex items-center justify-center flex-shrink-0`}>
  156. {getNodeIcon()}
  157. </div>
  158. {/* 节点名称和人员信息 */}
  159. <div className="flex flex-col gap-1">
  160. <div className="font-semibold text-base text-gray-900">
  161. {data?.label || data?.nodeName || '节点'}
  162. </div>
  163. <div className="text-sm text-gray-600">
  164. {getExecutorInfo()}
  165. </div>
  166. </div>
  167. </div>
  168. );
  169. }
  170. export default function WorkJobDetail() {
  171. const navigate = useNavigate();
  172. const [searchParams] = useSearchParams();
  173. const jobId = searchParams.get('id');
  174. const [jobDetail, setJobDetail] = useState<WorkJobVO | null>(null);
  175. const [loading, setLoading] = useState(true);
  176. // ReactFlow 状态
  177. const [nodes, setNodes, onNodesChange] = useNodesState([]);
  178. const [edges, setEdges, onEdgesChange] = useEdgesState([]);
  179. const reactFlowWrapper = useRef<HTMLDivElement>(null);
  180. const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);
  181. // 获取作业详情
  182. useEffect(() => {
  183. if (jobId) {
  184. loadJobDetail();
  185. }
  186. }, [jobId]);
  187. const loadJobDetail = async () => {
  188. if (!jobId) return;
  189. try {
  190. setLoading(true);
  191. const response = await workJobApi.selectWorkflowWorkById(Number(jobId));
  192. // 处理响应数据,可能包含 data 字段
  193. const data = (response as any)?.data || response;
  194. setJobDetail(data);
  195. console.log('作业详情数据:', data);
  196. // 加载完成后,解析 designContent 并渲染流程
  197. if (data?.designContent) {
  198. loadWorkflowFromDesignContent(data);
  199. }
  200. } catch (error: any) {
  201. console.error('获取作业详情失败:', error);
  202. } finally {
  203. setLoading(false);
  204. }
  205. };
  206. // 从 designContent 加载流程并渲染到 ReactFlow
  207. const loadWorkflowFromDesignContent = (jobData: WorkJobVO) => {
  208. try {
  209. // 解析 designContent
  210. let designContentData: any = null;
  211. if (jobData.designContent) {
  212. try {
  213. designContentData = typeof jobData.designContent === 'string'
  214. ? JSON.parse(jobData.designContent)
  215. : jobData.designContent;
  216. } catch (e) {
  217. console.warn('解析 designContent 失败:', e);
  218. return;
  219. }
  220. }
  221. if (!designContentData || !designContentData.nodes || !Array.isArray(designContentData.nodes)) {
  222. console.warn('designContent 数据格式不正确');
  223. return;
  224. }
  225. // 构建节点映射(通过 uuid)
  226. const nodeMap = new Map<string, any>();
  227. if (jobData.workflowWorkNodeDOList && Array.isArray(jobData.workflowWorkNodeDOList)) {
  228. jobData.workflowWorkNodeDOList.forEach((node: any) => {
  229. if (node.uuid) {
  230. nodeMap.set(node.uuid, node);
  231. }
  232. } );
  233. }
  234. const getNodeStatus = (nodeUuid: string): 'completed' | 'in_progress' | 'pending' => {
  235. const workNode = nodeMap.get(nodeUuid);
  236. if (!workNode) return 'pending';
  237. if (workNode.approvalStatus === 'approved') {
  238. return 'completed';
  239. } else if (workNode.approvalStatus === 'unaudited' || workNode.approvalStatus === 'pending') {
  240. // 检查父节点是否已完成
  241. if (!workNode.parentUuid || workNode.parentUuid === '') {
  242. return 'completed'; // 根节点
  243. }
  244. const parentNode = nodeMap.get(workNode.parentUuid);
  245. if (parentNode && parentNode.approvalStatus === 'approved') {
  246. return 'in_progress';
  247. }
  248. return 'pending';
  249. }
  250. return 'pending';
  251. };
  252. // 转换为 ReactFlow 的 nodes
  253. // 将横向布局转换为纵向布局:交换 x 和 y 坐标
  254. const importedNodes: Node[] = designContentData.nodes.map((node: any) => {
  255. const nodeId = node.id || node.uuid;
  256. const nodeData = node.data || {};
  257. const workNode = nodeMap.get(nodeId);
  258. const status = getNodeStatus(nodeId);
  259. // 获取原始位置
  260. const originalPosition = node.position || { x: 0, y: 0 };
  261. // 交换 x 和 y 坐标,实现横向到纵向的转换
  262. // 同时应用缩放因子,使节点间距更紧凑(0.6 表示缩小到原来的 60%)
  263. const spacingScale = 0.6;
  264. const verticalPosition = {
  265. x: originalPosition.y * spacingScale,
  266. y: originalPosition.x * spacingScale,
  267. };
  268. return {
  269. id: nodeId,
  270. type: node.type || 'createJob',
  271. position: verticalPosition,
  272. data: {
  273. ...nodeData,
  274. label: nodeData.label || node.label || node.nodeName || workNode?.nodeName || '节点',
  275. type: node.type || nodeData.type || 'createJob',
  276. status: status,
  277. workNode: workNode, // 传递 workNode 以便获取人员信息
  278. },
  279. };
  280. });
  281. // 转换为 ReactFlow 的 edges
  282. // 统一使用垂直布局:所有连接线都从节点底部垂直向下
  283. const importedEdges: Edge[] = (designContentData.edges || []).map((edge: any) => {
  284. // 检查节点位置,确保 source 在上,target 在下
  285. const sourceNode = importedNodes.find((n: Node) => n.id === edge.source);
  286. const targetNode = importedNodes.find((n: Node) => n.id === edge.target);
  287. // 如果 target 在 source 上方,交换它们(确保连接线从上到下)
  288. let finalSource = edge.source;
  289. let finalTarget = edge.target;
  290. if (sourceNode && targetNode && targetNode.position.y < sourceNode.position.y) {
  291. finalSource = edge.target;
  292. finalTarget = edge.source;
  293. }
  294. // 使用最终确定的 source 节点状态来确定边的颜色
  295. const sourceStatus = getNodeStatus(finalSource);
  296. // 根据状态确定边的颜色
  297. let edgeColor = '#d1d5db'; // 默认灰色
  298. let edgeStyle = 'dashed';
  299. if (sourceStatus === 'completed') {
  300. edgeColor = '#10b981'; // 绿色
  301. edgeStyle = 'solid';
  302. } else if (sourceStatus === 'in_progress') {
  303. edgeColor = '#3b82f6'; // 蓝色
  304. edgeStyle = 'solid';
  305. }
  306. return {
  307. id: edge.id || `${finalSource}-${finalTarget}`,
  308. source: finalSource,
  309. target: finalTarget,
  310. sourceHandle: 'bottom-source', // 统一使用底部
  311. targetHandle: 'top-target', // 统一使用顶部
  312. // 不指定 type,使用 ReactFlow 默认的连线效果
  313. style: {
  314. strokeWidth: 4,
  315. stroke: edgeColor,
  316. strokeDasharray: edgeStyle === 'dashed' ? '5,5' : undefined,
  317. },
  318. };
  319. });
  320. console.log('导入的节点:', importedNodes);
  321. console.log('导入的连线:', importedEdges);
  322. setNodes(importedNodes);
  323. setEdges(importedEdges);
  324. // 自动适应视图
  325. if (reactFlowInstance && importedNodes.length > 0) {
  326. setTimeout(() => {
  327. reactFlowInstance.fitView({ padding: 0.2, duration: 300 });
  328. }, 100);
  329. }
  330. } catch (error) {
  331. console.error('加载流程失败:', error);
  332. }
  333. };
  334. // 格式化状态文本
  335. const getStatusText = (status: string | number | undefined): string => {
  336. const statusMap: Record<string, string> = {
  337. 'pending': '待执行',
  338. 'running': '执行中',
  339. 'completed': '执行完成',
  340. 'rejected': '已退回',
  341. 'skipped': '已跳过',
  342. '进行中': '执行中',
  343. '已完成': '执行完成',
  344. '待开始': '待执行',
  345. '已取消': '已取消',
  346. };
  347. return statusMap[String(status).toLowerCase()] || String(status) || '未知';
  348. };
  349. // 格式化状态样式
  350. const getStatusClassName = (status: string | number | undefined): string => {
  351. const statusStr = String(status).toLowerCase();
  352. if (statusStr === 'running' || statusStr === '执行中') {
  353. return 'bg-blue-100 text-blue-700';
  354. }
  355. if (statusStr === 'completed' || statusStr === '执行完成' || statusStr === '已完成') {
  356. return 'bg-green-100 text-green-700';
  357. }
  358. if (statusStr === 'pending' || statusStr === '待执行' || statusStr === '待开始') {
  359. return 'bg-gray-100 text-gray-700';
  360. }
  361. return 'bg-gray-100 text-gray-700';
  362. };
  363. // 构建工作流步骤
  364. const buildWorkflowSteps = (): WorkflowStep[] => {
  365. // 这里应该根据实际的作业数据构建步骤
  366. // 目前使用模拟数据,后续需要根据实际API返回的数据结构调整
  367. const steps: WorkflowStep[] = [
  368. {
  369. id: '1',
  370. type: 'initiator',
  371. title: '发起人',
  372. assigneeName: jobDetail?.initiatorName || jobDetail?.initiator || '张三',
  373. time: jobDetail?.initiationTime || jobDetail?.initiateTime
  374. ? new Date((jobDetail.initiationTime || jobDetail.initiateTime) as string | number | Date).toLocaleString('zh-CN', {
  375. year: 'numeric',
  376. month: '2-digit',
  377. day: '2-digit',
  378. hour: '2-digit',
  379. minute: '2-digit',
  380. second: '2-digit'
  381. })
  382. : '2024-01-15 10:30:00',
  383. status: 'completed',
  384. icon: <CheckCircle2 className="w-5 h-5" />,
  385. },
  386. {
  387. id: '2',
  388. type: 'review',
  389. title: '审核/确认',
  390. assigneeName: '李四',
  391. time: '2024-01-15 10:35:00',
  392. status: 'in_progress',
  393. icon: <Settings className="w-5 h-5" />,
  394. hasDetail: true,
  395. },
  396. {
  397. id: '3',
  398. type: 'input',
  399. title: '录入/表单',
  400. assigneeName: '王五',
  401. status: 'pending',
  402. icon: <FileText className="w-5 h-5" />,
  403. },
  404. {
  405. id: '4',
  406. type: 'isolation',
  407. title: '隔离/方案',
  408. assigneeName: '赵六',
  409. status: 'pending',
  410. icon: <Shield className="w-5 h-5" />,
  411. },
  412. {
  413. id: '5',
  414. type: 'lock',
  415. title: '取锁/共锁',
  416. assigneeName: '钱七',
  417. status: 'pending',
  418. icon: <Lock className="w-5 h-5" />,
  419. },
  420. {
  421. id: '6',
  422. type: 'unlock',
  423. title: '还锁',
  424. assigneeName: '孙八',
  425. status: 'pending',
  426. icon: <Lock className="w-5 h-5" />,
  427. },
  428. {
  429. id: '7',
  430. type: 'complete',
  431. title: '完成/结束',
  432. status: 'pending',
  433. icon: <Flag className="w-5 h-5" />,
  434. },
  435. ];
  436. return steps;
  437. };
  438. const workflowSteps = buildWorkflowSteps();
  439. // 构建流转记录数据
  440. const buildFlowRecords = (): FlowRecord[] => {
  441. if (!jobDetail?.workflowWorkNodeDOList || !Array.isArray(jobDetail.workflowWorkNodeDOList)) {
  442. return [];
  443. }
  444. const records: FlowRecord[] = jobDetail.workflowWorkNodeDOList.map((node: any, index: number) => {
  445. // 根据审批状态确定任务状态
  446. let taskStatus: 'completed' | 'in_progress' | 'pending' = 'pending';
  447. if (node.approvalStatus === 'approved') {
  448. taskStatus = 'completed';
  449. } else if (node.approvalStatus === 'unaudited' || node.approvalStatus === 'pending') {
  450. // 检查是否是第一个节点或者前面的节点已完成
  451. if (index === 0) {
  452. taskStatus = 'completed';
  453. } else {
  454. const prevNode = jobDetail.workflowWorkNodeDOList[index - 1];
  455. if (prevNode && prevNode.approvalStatus === 'approved') {
  456. taskStatus = 'in_progress';
  457. } else {
  458. taskStatus = 'pending';
  459. }
  460. }
  461. }
  462. // 格式化时间
  463. const startTime = node.createTime ? formatDateWithFormat(node.createTime) : undefined;
  464. // 获取执行人(如果有)
  465. const executor = node.workerUserId || node.initiatorName || '';
  466. // 获取描述信息
  467. let description = node.approvalOpinion || '';
  468. if (!description || description === 'pending') {
  469. if (taskStatus === 'completed') {
  470. description = '任务已完成';
  471. } else if (taskStatus === 'in_progress') {
  472. description = '任务正在进行中';
  473. } else {
  474. description = '等待开始';
  475. }
  476. }
  477. return {
  478. id: String(node.id || index),
  479. taskNode: node.nodeName || '未知节点',
  480. executor: executor,
  481. startTime: startTime,
  482. endTime: undefined,
  483. taskStatus: taskStatus,
  484. executionDescription: description,
  485. duration: undefined,
  486. };
  487. });
  488. return records;
  489. };
  490. // 获取流转记录状态文本
  491. const getFlowRecordStatusText = (status: FlowRecord['taskStatus']): string => {
  492. const statusMap: Record<FlowRecord['taskStatus'], string> = {
  493. 'completed': '已完成',
  494. 'in_progress': '执行中',
  495. 'pending': '待处理',
  496. };
  497. return statusMap[status] || '未知';
  498. };
  499. // 获取流转记录状态样式
  500. const getFlowRecordStatusClassName = (status: FlowRecord['taskStatus']): string => {
  501. const statusMap: Record<FlowRecord['taskStatus'], string> = {
  502. 'completed': 'bg-green-100 text-green-700',
  503. 'in_progress': 'bg-blue-100 text-blue-700',
  504. 'pending': 'bg-gray-100 text-gray-700',
  505. };
  506. return statusMap[status] || 'bg-gray-100 text-gray-700';
  507. };
  508. const flowRecords = buildFlowRecords();
  509. if (loading) {
  510. return (
  511. <div className="min-h-screen bg-gray-50 flex items-center justify-center">
  512. <div className="text-gray-500">加载中...</div>
  513. </div>
  514. );
  515. }
  516. if (!jobDetail) {
  517. return (
  518. <div className="min-h-screen bg-gray-50 flex items-center justify-center">
  519. <div className="text-gray-500">作业不存在</div>
  520. </div>
  521. );
  522. }
  523. return (
  524. <div className="h-screen bg-gray-50 flex flex-col">
  525. <div className="w-full max-w-none mx-auto px-8 py-6 flex-1 flex flex-col min-h-0">
  526. {/* 头部区域 */}
  527. <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6 flex-shrink-0" style={{ marginLeft: '20px', marginRight: '20px' }}>
  528. <div className="flex items-start justify-between">
  529. <div className="flex-1">
  530. {/* 编号 */}
  531. <div className="text-sm text-gray-600 mb-2">
  532. 编号:<span className="font-medium text-gray-900">{jobDetail?.orderNo || jobDetail?.code || '-'}</span>
  533. </div>
  534. {/* 标题和状态按钮(同一行) */}
  535. <div className="flex items-center gap-3 mb-3">
  536. <h1 className="text-2xl font-semibold text-gray-900">
  537. {jobDetail?.name || '-'}
  538. </h1>
  539. <span className={`inline-flex px-4 py-1.5 rounded-full text-sm font-medium ${getStatusClassName(jobDetail?.status)}`}>
  540. {getStatusText(jobDetail?.status)}
  541. </span>
  542. </div>
  543. {/* 发起人信息 */}
  544. <div className="flex items-center gap-2 text-sm text-gray-600">
  545. <div className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
  546. <User className="w-3.5 h-3.5 text-blue-600" />
  547. </div>
  548. <span>{jobDetail?.initiatorName || jobDetail?.initiator || '-'}</span>
  549. <span className="text-gray-300">|</span>
  550. <span>
  551. {jobDetail?.initiationTime
  552. ? formatDateWithFormat(jobDetail.initiationTime as string | number | Date)
  553. : '-'} 提交
  554. </span>
  555. </div>
  556. </div>
  557. {/* 返回按钮 */}
  558. <button
  559. onClick={() => navigate(-1)}
  560. className="flex items-center justify-center w-10 h-10 rounded-lg border border-gray-300 hover:bg-gray-50 hover:border-gray-400 transition-colors flex-shrink-0"
  561. title="返回作业管理"
  562. >
  563. <ArrowLeft className="w-5 h-5 text-gray-600" />
  564. </button>
  565. </div>
  566. </div>
  567. {/* 任务详情和执行记录区域 - 左右两栏布局 */}
  568. <div className="flex-1 flex gap-8 mb-6 min-h-0">
  569. {/* 左侧:任务详情 */}
  570. <div className="bg-white rounded-xl shadow-sm border border-gray-200 flex-1 flex flex-col min-h-0" style={{ marginLeft: '20px',maxHeight:'750px' }}>
  571. <div className="px-6 py-4 border-b border-gray-200">
  572. <h2 className="text-lg font-semibold text-blue-600 flex items-center gap-2">
  573. <FileText className="w-5 h-5" />
  574. 作业流程
  575. </h2>
  576. </div>
  577. <div className="flex-1 overflow-y-auto p-6">
  578. {/* 作业流程渲染 - 使用 ReactFlow */}
  579. {(() => {
  580. if (!jobDetail?.designContent || nodes.length === 0) {
  581. return (
  582. <div className="flex items-center justify-center h-full text-gray-400">
  583. {loading ? '加载中...' : '暂无流程数据'}
  584. </div>
  585. );
  586. }
  587. return (
  588. <div className="relative" style={{ minHeight: '100%', height: '600px' }} ref={reactFlowWrapper}>
  589. <ReactFlow
  590. nodes={nodes}
  591. edges={edges}
  592. onNodesChange={onNodesChange}
  593. onEdgesChange={onEdgesChange}
  594. nodeTypes={nodeTypes}
  595. fitView
  596. onInit={setReactFlowInstance}
  597. nodesDraggable={false}
  598. nodesConnectable={false}
  599. elementsSelectable={false}
  600. connectionMode={ConnectionMode.Loose}
  601. defaultEdgeOptions={{
  602. style: { strokeWidth: 4 },
  603. }}
  604. >
  605. </ReactFlow>
  606. </div>
  607. );
  608. })()}
  609. </div>
  610. </div>
  611. {/* 右侧:执行记录 */}
  612. <div className="bg-white rounded-xl shadow-sm border border-gray-200 flex-1 flex flex-col min-h-0 flex-shrink-0" style={{ marginRight: '20px',maxHeight:'750px' }}>
  613. <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
  614. <h2 className="text-lg font-semibold text-blue-600 flex items-center gap-2">
  615. <Clock className="w-5 h-5" />
  616. 执行记录
  617. </h2>
  618. </div>
  619. <div className="flex-1 overflow-y-auto p-4">
  620. <div className="space-y-3">
  621. {flowRecords.map((record, index) => {
  622. const isCompleted = record.taskStatus === 'completed';
  623. const isInProgress = record.taskStatus === 'in_progress';
  624. const isPending = record.taskStatus === 'pending';
  625. // 根据状态获取描述信息
  626. let description = record.executionDescription || '';
  627. if (!description) {
  628. if (isCompleted) {
  629. description = '任务已完成';
  630. } else if (isInProgress) {
  631. description = '任务正在进行中';
  632. } else {
  633. description = '等待开始';
  634. }
  635. }
  636. return (
  637. <div
  638. key={record.id}
  639. className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow"
  640. style={{
  641. borderLeftWidth: '4px',
  642. borderLeftColor: isCompleted ? '#10b981' : isInProgress ? '#3b82f6' : '#9ca3af',
  643. }}
  644. >
  645. <div className="flex items-start justify-between">
  646. <div className="flex-1">
  647. {/* 任务节点名称 */}
  648. <div className="font-semibold text-base text-gray-900 mb-2">
  649. {record.taskNode}
  650. </div>
  651. {/* 执行人信息 */}
  652. <div className="text-sm text-gray-600 mb-2">
  653. {record.executor ? (
  654. <>
  655. {record.taskNode === '作业申请' ? '发起人' :
  656. record.taskNode === '审核审批' ? '审核人' :
  657. record.taskNode === '上锁操作' || record.taskNode === '共锁操作' ? '操作人' :
  658. '执行人'}: {record.executor}
  659. </>
  660. ) : '待处理'}
  661. </div>
  662. {/* 描述信息 */}
  663. <div className="text-sm text-gray-500">
  664. {description}
  665. </div>
  666. {/* 时间信息 */}
  667. {record.startTime && (
  668. <div className="text-xs text-gray-400 mt-2">
  669. {record.startTime}
  670. </div>
  671. )}
  672. </div>
  673. {/* 状态标签 */}
  674. <div className="ml-4 flex-shrink-0">
  675. <span className={`inline-flex px-3 py-1 rounded-full text-xs font-medium ${getFlowRecordStatusClassName(record.taskStatus)}`}>
  676. {getFlowRecordStatusText(record.taskStatus)}
  677. </span>
  678. </div>
  679. </div>
  680. </div>
  681. );
  682. })}
  683. </div>
  684. </div>
  685. </div>
  686. </div>
  687. </div>
  688. </div>
  689. );
  690. }