|
@@ -1,4 +1,4 @@
|
|
|
-import React, { useState, useEffect } from 'react';
|
|
|
|
|
|
|
+import React, { useState, useEffect, useRef } from 'react';
|
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
|
import {
|
|
import {
|
|
|
User,
|
|
User,
|
|
@@ -9,20 +9,25 @@ import {
|
|
|
Lock,
|
|
Lock,
|
|
|
Flag,
|
|
Flag,
|
|
|
Star,
|
|
Star,
|
|
|
- Play,
|
|
|
|
|
- Check,
|
|
|
|
|
- Pause,
|
|
|
|
|
- Send,
|
|
|
|
|
- ArrowLeftRight,
|
|
|
|
|
- UserPlus,
|
|
|
|
|
- Plus,
|
|
|
|
|
ArrowLeft,
|
|
ArrowLeft,
|
|
|
- X
|
|
|
|
|
|
|
+ Clock,
|
|
|
|
|
+ List
|
|
|
} from 'lucide-react';
|
|
} from 'lucide-react';
|
|
|
-import { Button } from './ui/button';
|
|
|
|
|
-import { Tabs } from 'antd';
|
|
|
|
|
|
|
+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 } from '../api/WorkJob';
|
|
|
-import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from './ui/table';
|
|
|
|
|
import { formatDateWithFormat } from '../utils/formatTime';
|
|
import { formatDateWithFormat } from '../utils/formatTime';
|
|
|
|
|
|
|
|
interface WorkflowStep {
|
|
interface WorkflowStep {
|
|
@@ -49,6 +54,135 @@ 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} />,
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 简化的自定义节点组件(只读模式)
|
|
|
|
|
+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 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 = () => {
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有从 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 (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 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>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export default function WorkJobDetail() {
|
|
export default function WorkJobDetail() {
|
|
|
const navigate = useNavigate();
|
|
const navigate = useNavigate();
|
|
|
const [searchParams] = useSearchParams();
|
|
const [searchParams] = useSearchParams();
|
|
@@ -56,7 +190,12 @@ 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);
|
|
|
- const [activeTab, setActiveTab] = useState('details');
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // ReactFlow 状态
|
|
|
|
|
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
|
|
|
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
|
|
|
+ const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
|
|
|
+ const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);
|
|
|
|
|
|
|
|
// 获取作业详情
|
|
// 获取作业详情
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -70,7 +209,15 @@ export default function WorkJobDetail() {
|
|
|
try {
|
|
try {
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
const response = await workJobApi.selectWorkflowWorkById(Number(jobId));
|
|
const response = await workJobApi.selectWorkflowWorkById(Number(jobId));
|
|
|
- setJobDetail(response as any);
|
|
|
|
|
|
|
+ // 处理响应数据,可能包含 data 字段
|
|
|
|
|
+ const data = (response as any)?.data || response;
|
|
|
|
|
+ setJobDetail(data);
|
|
|
|
|
+ console.log('作业详情数据:', data);
|
|
|
|
|
+
|
|
|
|
|
+ // 加载完成后,解析 designContent 并渲染流程
|
|
|
|
|
+ if (data?.designContent) {
|
|
|
|
|
+ loadWorkflowFromDesignContent(data);
|
|
|
|
|
+ }
|
|
|
} catch (error: any) {
|
|
} catch (error: any) {
|
|
|
console.error('获取作业详情失败:', error);
|
|
console.error('获取作业详情失败:', error);
|
|
|
} finally {
|
|
} finally {
|
|
@@ -78,6 +225,150 @@ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!designContentData || !designContentData.nodes || !Array.isArray(designContentData.nodes)) {
|
|
|
|
|
+ console.warn('designContent 数据格式不正确');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建节点映射(通过 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';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 转换为 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
// 格式化状态文本
|
|
// 格式化状态文本
|
|
|
const getStatusText = (status: string | number | undefined): string => {
|
|
const getStatusText = (status: string | number | undefined): string => {
|
|
|
const statusMap: Record<string, string> = {
|
|
const statusMap: Record<string, string> = {
|
|
@@ -120,7 +411,7 @@ export default function WorkJobDetail() {
|
|
|
title: '发起人',
|
|
title: '发起人',
|
|
|
assigneeName: jobDetail?.initiatorName || jobDetail?.initiator || '张三',
|
|
assigneeName: jobDetail?.initiatorName || jobDetail?.initiator || '张三',
|
|
|
time: jobDetail?.initiationTime || jobDetail?.initiateTime
|
|
time: jobDetail?.initiationTime || jobDetail?.initiateTime
|
|
|
- ? new Date(jobDetail.initiationTime || jobDetail.initiateTime).toLocaleString('zh-CN', {
|
|
|
|
|
|
|
+ ? new Date((jobDetail.initiationTime || jobDetail.initiateTime) as string | number | Date).toLocaleString('zh-CN', {
|
|
|
year: 'numeric',
|
|
year: 'numeric',
|
|
|
month: '2-digit',
|
|
month: '2-digit',
|
|
|
day: '2-digit',
|
|
day: '2-digit',
|
|
@@ -189,54 +480,59 @@ export default function WorkJobDetail() {
|
|
|
|
|
|
|
|
// 构建流转记录数据
|
|
// 构建流转记录数据
|
|
|
const buildFlowRecords = (): FlowRecord[] => {
|
|
const buildFlowRecords = (): FlowRecord[] => {
|
|
|
- // 这里应该根据实际的作业数据构建流转记录
|
|
|
|
|
- // 目前使用模拟数据,后续需要根据实际API返回的数据结构调整
|
|
|
|
|
- const initTime = jobDetail?.initiationTime || jobDetail?.initiateTime;
|
|
|
|
|
- const startTime = initTime ? formatDateWithFormat(initTime) : '2024-01-15 10:30:00';
|
|
|
|
|
-
|
|
|
|
|
- // 计算结束时间(如果开始时间是时间戳,加5秒)
|
|
|
|
|
- let endTime: string | undefined;
|
|
|
|
|
- if (initTime) {
|
|
|
|
|
- const startTimestamp = typeof initTime === 'number'
|
|
|
|
|
- ? (initTime > 1000000000000 ? initTime : initTime * 1000)
|
|
|
|
|
- : new Date(initTime).getTime();
|
|
|
|
|
- endTime = formatDateWithFormat(new Date(startTimestamp + 5000));
|
|
|
|
|
- } else {
|
|
|
|
|
- endTime = '2024-01-15 10:30:05';
|
|
|
|
|
|
|
+ if (!jobDetail?.workflowWorkNodeDOList || !Array.isArray(jobDetail.workflowWorkNodeDOList)) {
|
|
|
|
|
+ return [];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const records: FlowRecord[] = [
|
|
|
|
|
- {
|
|
|
|
|
- id: '1',
|
|
|
|
|
- taskNode: '发起人',
|
|
|
|
|
- executor: jobDetail?.initiatorName || jobDetail?.initiator || '张三',
|
|
|
|
|
- startTime,
|
|
|
|
|
- endTime,
|
|
|
|
|
- taskStatus: 'completed',
|
|
|
|
|
- executionDescription: '查看表单',
|
|
|
|
|
- duration: '5秒',
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: '2',
|
|
|
|
|
- taskNode: '审核/确认',
|
|
|
|
|
- executor: '李四',
|
|
|
|
|
- startTime: '2024-01-15 10:35:00',
|
|
|
|
|
- endTime: undefined,
|
|
|
|
|
- taskStatus: 'in_progress',
|
|
|
|
|
- executionDescription: '查看表单',
|
|
|
|
|
- duration: '进行中',
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: '3',
|
|
|
|
|
- taskNode: '录入/表单',
|
|
|
|
|
- executor: '王五',
|
|
|
|
|
- startTime: undefined,
|
|
|
|
|
|
|
+ const records: FlowRecord[] = jobDetail.workflowWorkNodeDOList.map((node: any, index: number) => {
|
|
|
|
|
+ // 根据审批状态确定任务状态
|
|
|
|
|
+ let taskStatus: 'completed' | 'in_progress' | 'pending' = 'pending';
|
|
|
|
|
+ if (node.approvalStatus === 'approved') {
|
|
|
|
|
+ taskStatus = 'completed';
|
|
|
|
|
+ } else if (node.approvalStatus === 'unaudited' || node.approvalStatus === 'pending') {
|
|
|
|
|
+ // 检查是否是第一个节点或者前面的节点已完成
|
|
|
|
|
+ if (index === 0) {
|
|
|
|
|
+ taskStatus = 'completed';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const prevNode = jobDetail.workflowWorkNodeDOList[index - 1];
|
|
|
|
|
+ if (prevNode && prevNode.approvalStatus === 'approved') {
|
|
|
|
|
+ taskStatus = 'in_progress';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ taskStatus = 'pending';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 格式化时间
|
|
|
|
|
+ const startTime = node.createTime ? formatDateWithFormat(node.createTime) : undefined;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取执行人(如果有)
|
|
|
|
|
+ const executor = node.workerUserId || node.initiatorName || '';
|
|
|
|
|
+
|
|
|
|
|
+ // 获取描述信息
|
|
|
|
|
+ let description = node.approvalOpinion || '';
|
|
|
|
|
+ if (!description || description === 'pending') {
|
|
|
|
|
+ if (taskStatus === 'completed') {
|
|
|
|
|
+ description = '任务已完成';
|
|
|
|
|
+ } else if (taskStatus === 'in_progress') {
|
|
|
|
|
+ description = '任务正在进行中';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ description = '等待开始';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: String(node.id || index),
|
|
|
|
|
+ taskNode: node.nodeName || '未知节点',
|
|
|
|
|
+ executor: executor,
|
|
|
|
|
+ startTime: startTime,
|
|
|
endTime: undefined,
|
|
endTime: undefined,
|
|
|
- taskStatus: 'pending',
|
|
|
|
|
- executionDescription: undefined,
|
|
|
|
|
|
|
+ taskStatus: taskStatus,
|
|
|
|
|
+ executionDescription: description,
|
|
|
duration: undefined,
|
|
duration: undefined,
|
|
|
- },
|
|
|
|
|
- ];
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
return records;
|
|
return records;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -260,12 +556,6 @@ export default function WorkJobDetail() {
|
|
|
return statusMap[status] || 'bg-gray-100 text-gray-700';
|
|
return statusMap[status] || 'bg-gray-100 text-gray-700';
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 处理查看表单点击
|
|
|
|
|
- const handleViewForm = (record: FlowRecord) => {
|
|
|
|
|
- // TODO: 实现查看表单功能
|
|
|
|
|
- console.log('查看表单:', record);
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
const flowRecords = buildFlowRecords();
|
|
const flowRecords = buildFlowRecords();
|
|
|
|
|
|
|
|
if (loading) {
|
|
if (loading) {
|
|
@@ -286,21 +576,23 @@ export default function WorkJobDetail() {
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="h-screen bg-gray-50 flex flex-col">
|
|
<div className="h-screen bg-gray-50 flex flex-col">
|
|
|
- <div className="max-w-7xl mx-auto px-4 py-6 flex-1 flex flex-col min-h-0 w-full">
|
|
|
|
|
|
|
+ <div className="w-full max-w-none mx-auto px-8 py-6 flex-1 flex flex-col min-h-0">
|
|
|
{/* 头部区域 */}
|
|
{/* 头部区域 */}
|
|
|
- <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6 flex-shrink-0">
|
|
|
|
|
|
|
+ <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6 flex-shrink-0" style={{ marginLeft: '20px', marginRight: '20px' }}>
|
|
|
|
|
+ <div className="flex items-start justify-between">
|
|
|
|
|
+ <div className="flex-1">
|
|
|
{/* 编号 */}
|
|
{/* 编号 */}
|
|
|
<div className="text-sm text-gray-600 mb-2">
|
|
<div className="text-sm text-gray-600 mb-2">
|
|
|
- 编号:<span className="font-medium text-gray-900">{jobDetail.orderNo || jobDetail.code || 'Work1766368222538'}</span>
|
|
|
|
|
|
|
+ 编号:<span className="font-medium text-gray-900">{jobDetail?.orderNo || jobDetail?.code || '-'}</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* 标题和状态按钮(同一行) */}
|
|
{/* 标题和状态按钮(同一行) */}
|
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
|
<h1 className="text-2xl font-semibold text-gray-900">
|
|
<h1 className="text-2xl font-semibold text-gray-900">
|
|
|
- {jobDetail.name || 'test'}
|
|
|
|
|
|
|
+ {jobDetail?.name || '-'}
|
|
|
</h1>
|
|
</h1>
|
|
|
- <span className={`inline-flex px-4 py-1.5 rounded-full text-sm font-medium ${getStatusClassName(jobDetail.status)}`}>
|
|
|
|
|
- {getStatusText(jobDetail.status)}
|
|
|
|
|
|
|
+ <span className={`inline-flex px-4 py-1.5 rounded-full text-sm font-medium ${getStatusClassName(jobDetail?.status)}`}>
|
|
|
|
|
+ {getStatusText(jobDetail?.status)}
|
|
|
</span>
|
|
</span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -309,281 +601,155 @@ export default function WorkJobDetail() {
|
|
|
<div className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
|
<div className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center flex-shrink-0">
|
|
|
<User className="w-3.5 h-3.5 text-blue-600" />
|
|
<User className="w-3.5 h-3.5 text-blue-600" />
|
|
|
</div>
|
|
</div>
|
|
|
- <span>{jobDetail.initiatorName || jobDetail.initiator || '博士安全'}</span>
|
|
|
|
|
|
|
+ <span>{jobDetail?.initiatorName || jobDetail?.initiator || '-'}</span>
|
|
|
<span className="text-gray-300">|</span>
|
|
<span className="text-gray-300">|</span>
|
|
|
- <span>
|
|
|
|
|
- {jobDetail.initiationTime || jobDetail.initiateTime
|
|
|
|
|
- ? new Date(jobDetail.initiationTime || jobDetail.initiateTime).toLocaleString('zh-CN', {
|
|
|
|
|
- year: 'numeric',
|
|
|
|
|
- month: '2-digit',
|
|
|
|
|
- day: '2-digit',
|
|
|
|
|
- hour: '2-digit',
|
|
|
|
|
- minute: '2-digit',
|
|
|
|
|
- second: '2-digit'
|
|
|
|
|
- }).replace(/\//g, '/')
|
|
|
|
|
- : '2025/12/22 09:50:23'} 提交
|
|
|
|
|
- </span>
|
|
|
|
|
|
|
+ <span>
|
|
|
|
|
+ {jobDetail?.initiationTime
|
|
|
|
|
+ ? formatDateWithFormat(jobDetail.initiationTime as string | number | Date)
|
|
|
|
|
+ : '-'} 提交
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 返回按钮 */}
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => navigate(-1)}
|
|
|
|
|
+ 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"
|
|
|
|
|
+ title="返回作业管理"
|
|
|
|
|
+ >
|
|
|
|
|
+ <ArrowLeft className="w-5 h-5 text-gray-600" />
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {/* 导航标签和内容区域 - 撑满剩余空间 */}
|
|
|
|
|
- <div className="bg-white rounded-xl shadow-sm border border-gray-200 flex-1 flex flex-col min-h-0 mb-6">
|
|
|
|
|
- <style>{`
|
|
|
|
|
- .work-job-tabs .ant-tabs-nav {
|
|
|
|
|
- margin-bottom: 0;
|
|
|
|
|
- padding: 0 24px;
|
|
|
|
|
- }
|
|
|
|
|
- .work-job-tabs .ant-tabs-tab {
|
|
|
|
|
- padding: 16px 0;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- color: #6b7280;
|
|
|
|
|
- }
|
|
|
|
|
- .work-job-tabs .ant-tabs-tab-active {
|
|
|
|
|
- color: #2563eb !important;
|
|
|
|
|
- }
|
|
|
|
|
- .work-job-tabs .ant-tabs-ink-bar {
|
|
|
|
|
- background: #2563eb !important;
|
|
|
|
|
- height: 2px !important;
|
|
|
|
|
- }
|
|
|
|
|
- .work-job-tabs .ant-tabs-content-holder {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- }
|
|
|
|
|
- .work-job-tabs .ant-tabs-tabpane {
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
- }
|
|
|
|
|
- `}</style>
|
|
|
|
|
- <Tabs
|
|
|
|
|
- activeKey={activeTab}
|
|
|
|
|
- onChange={setActiveTab}
|
|
|
|
|
- type="line"
|
|
|
|
|
- className="work-job-tabs flex-1 flex flex-col min-h-0"
|
|
|
|
|
- items={[
|
|
|
|
|
- {
|
|
|
|
|
- key: 'details',
|
|
|
|
|
- label: '任务详情',
|
|
|
|
|
- children: (
|
|
|
|
|
- <div className="relative pl-2 p-6">
|
|
|
|
|
- {/* 工作流步骤时间线 */}
|
|
|
|
|
- <div className="space-y-0">
|
|
|
|
|
- {workflowSteps.map((step, index) => {
|
|
|
|
|
- const isActive = step.status === 'in_progress';
|
|
|
|
|
- const isCompleted = step.status === 'completed';
|
|
|
|
|
- const isPending = step.status === 'pending';
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <div key={step.id} className="relative flex items-start pb-8 last:pb-0">
|
|
|
|
|
- {/* 左侧连接线 */}
|
|
|
|
|
- {index < workflowSteps.length - 1 && (
|
|
|
|
|
- <div
|
|
|
|
|
- className={`absolute left-[18px] top-[36px] w-0.5 ${
|
|
|
|
|
- isCompleted ? 'bg-blue-500' : 'bg-gray-300'
|
|
|
|
|
- }`}
|
|
|
|
|
- style={{ height: 'calc(100% - 8px)' }}
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
-
|
|
|
|
|
- {/* 图标圆圈 */}
|
|
|
|
|
- <div
|
|
|
|
|
- className={`relative z-10 flex items-center justify-center w-9 h-9 rounded-full border-2 flex-shrink-0 ${
|
|
|
|
|
- isActive
|
|
|
|
|
- ? 'bg-white border-blue-500 shadow-md text-blue-600'
|
|
|
|
|
- : isCompleted
|
|
|
|
|
- ? 'bg-green-500 border-green-500 text-white'
|
|
|
|
|
- : 'bg-white border-gray-300 text-gray-400'
|
|
|
|
|
- }`}
|
|
|
|
|
- >
|
|
|
|
|
- {step.icon}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* 任务详情和执行记录区域 - 左右两栏布局 */}
|
|
|
|
|
+ <div className="flex-1 flex gap-8 mb-6 min-h-0">
|
|
|
|
|
+ {/* 左侧:任务详情 */}
|
|
|
|
|
+ <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' }}>
|
|
|
|
|
+ <div className="px-6 py-4 border-b border-gray-200">
|
|
|
|
|
+ <h2 className="text-lg font-semibold text-blue-600 flex items-center gap-2">
|
|
|
|
|
+ <FileText className="w-5 h-5" />
|
|
|
|
|
+ 作业流程
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <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>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- {/* 右侧内容 */}
|
|
|
|
|
- <div className="flex-1 ml-4 pt-0.5">
|
|
|
|
|
- <div className="flex items-start justify-between">
|
|
|
|
|
- <div className="flex-1">
|
|
|
|
|
- <div className="flex items-center gap-2 mb-1.5">
|
|
|
|
|
- <h3 className="text-base font-medium text-gray-900">{step.title}</h3>
|
|
|
|
|
- </div>
|
|
|
|
|
- {step.assigneeName && (
|
|
|
|
|
- <div className="text-sm text-gray-600 mb-1">
|
|
|
|
|
- {step.assigneeName}
|
|
|
|
|
- {step.time && (
|
|
|
|
|
- <>
|
|
|
|
|
- <span className="mx-2 text-gray-300">·</span>
|
|
|
|
|
- <span>{step.time}</span>
|
|
|
|
|
- </>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
- {step.hasDetail && (
|
|
|
|
|
- <button className="flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700 mt-2">
|
|
|
|
|
- <Star className="w-4 h-4" />
|
|
|
|
|
- 查看详情
|
|
|
|
|
- </button>
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 状态标签 */}
|
|
|
|
|
- <div className="ml-4 flex-shrink-0">
|
|
|
|
|
- {isActive ? (
|
|
|
|
|
- <span className="inline-flex px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700 border border-blue-200">
|
|
|
|
|
- 执行中
|
|
|
|
|
- </span>
|
|
|
|
|
- ) : isCompleted ? (
|
|
|
|
|
- <span className="inline-flex px-3 py-1 rounded-lg text-xs font-medium bg-gray-100 text-gray-700">
|
|
|
|
|
- 已完成
|
|
|
|
|
- </span>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span className="inline-flex px-3 py-1 rounded-lg text-xs font-medium bg-gray-100 text-gray-700">
|
|
|
|
|
- 待处理
|
|
|
|
|
- </span>
|
|
|
|
|
- )}
|
|
|
|
|
- </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 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' }}>
|
|
|
|
|
+ <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
|
|
|
+ <h2 className="text-lg font-semibold text-blue-600 flex items-center gap-2">
|
|
|
|
|
+ <Clock className="w-5 h-5" />
|
|
|
|
|
+ 执行记录
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex-1 overflow-y-auto p-4">
|
|
|
|
|
+ <div className="space-y-3">
|
|
|
|
|
+ {flowRecords.map((record, index) => {
|
|
|
|
|
+ const isCompleted = record.taskStatus === 'completed';
|
|
|
|
|
+ const isInProgress = record.taskStatus === 'in_progress';
|
|
|
|
|
+ const isPending = record.taskStatus === 'pending';
|
|
|
|
|
+
|
|
|
|
|
+ // 根据状态获取描述信息
|
|
|
|
|
+ let description = record.executionDescription || '';
|
|
|
|
|
+ if (!description) {
|
|
|
|
|
+ if (isCompleted) {
|
|
|
|
|
+ description = '任务已完成';
|
|
|
|
|
+ } else if (isInProgress) {
|
|
|
|
|
+ description = '任务正在进行中';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ description = '等待开始';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ key={record.id}
|
|
|
|
|
+ className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ borderLeftWidth: '4px',
|
|
|
|
|
+ borderLeftColor: isCompleted ? '#10b981' : isInProgress ? '#3b82f6' : '#9ca3af',
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="flex items-start justify-between">
|
|
|
|
|
+ <div className="flex-1">
|
|
|
|
|
+ {/* 任务节点名称 */}
|
|
|
|
|
+ <div className="font-semibold text-base text-gray-900 mb-2">
|
|
|
|
|
+ {record.taskNode}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 执行人信息 */}
|
|
|
|
|
+ <div className="text-sm text-gray-600 mb-2">
|
|
|
|
|
+ {record.executor ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {record.taskNode === '作业申请' ? '发起人' :
|
|
|
|
|
+ record.taskNode === '审核审批' ? '审核人' :
|
|
|
|
|
+ record.taskNode === '上锁操作' || record.taskNode === '共锁操作' ? '操作人' :
|
|
|
|
|
+ '执行人'}: {record.executor}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : '待处理'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 描述信息 */}
|
|
|
|
|
+ <div className="text-sm text-gray-500">
|
|
|
|
|
+ {description}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 时间信息 */}
|
|
|
|
|
+ {record.startTime && (
|
|
|
|
|
+ <div className="text-xs text-gray-400 mt-2">
|
|
|
|
|
+ {record.startTime}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 状态标签 */}
|
|
|
|
|
+ <div className="ml-4 flex-shrink-0">
|
|
|
|
|
+ <span className={`inline-flex px-3 py-1 rounded-full text-xs font-medium ${getFlowRecordStatusClassName(record.taskStatus)}`}>
|
|
|
|
|
+ {getFlowRecordStatusText(record.taskStatus)}
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- ),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- key: 'flowchart',
|
|
|
|
|
- label: '流程图',
|
|
|
|
|
- children: (
|
|
|
|
|
- <div className="text-center py-12 text-gray-500 p-6">
|
|
|
|
|
- 流程图功能待开发
|
|
|
|
|
- </div>
|
|
|
|
|
- ),
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- key: 'history',
|
|
|
|
|
- label: '流转记录',
|
|
|
|
|
- children: (
|
|
|
|
|
- <div className="p-6">
|
|
|
|
|
- <div className="border border-gray-200 rounded-lg overflow-hidden">
|
|
|
|
|
- <Table>
|
|
|
|
|
- <TableHeader>
|
|
|
|
|
- <TableRow className="bg-gray-50 hover:bg-gray-50">
|
|
|
|
|
- <TableHead className="h-10 px-4 font-medium text-gray-700 bg-gray-50">任务节点</TableHead>
|
|
|
|
|
- <TableHead className="h-10 px-4 font-medium text-gray-700 bg-gray-50">执行人</TableHead>
|
|
|
|
|
- <TableHead className="h-10 px-4 font-medium text-gray-700 bg-gray-50">开始时间</TableHead>
|
|
|
|
|
- <TableHead className="h-10 px-4 font-medium text-gray-700 bg-gray-50">结束时间</TableHead>
|
|
|
|
|
- <TableHead className="h-10 px-4 font-medium text-gray-700 bg-gray-50">任务状态</TableHead>
|
|
|
|
|
- <TableHead className="h-10 px-4 font-medium text-gray-700 bg-gray-50">执行说明</TableHead>
|
|
|
|
|
- <TableHead className="h-10 px-4 font-medium text-gray-700 bg-gray-50">耗时</TableHead>
|
|
|
|
|
- </TableRow>
|
|
|
|
|
- </TableHeader>
|
|
|
|
|
- <TableBody>
|
|
|
|
|
- {flowRecords.length === 0 ? (
|
|
|
|
|
- <TableRow>
|
|
|
|
|
- <TableCell colSpan={7} className="text-center py-8 text-gray-500">
|
|
|
|
|
- 暂无流转记录
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- </TableRow>
|
|
|
|
|
- ) : (
|
|
|
|
|
- flowRecords.map((record) => (
|
|
|
|
|
- <TableRow key={record.id} className="hover:bg-gray-50">
|
|
|
|
|
- <TableCell className="px-4 py-3 text-gray-900">{record.taskNode}</TableCell>
|
|
|
|
|
- <TableCell className="px-4 py-3 text-gray-600">{record.executor}</TableCell>
|
|
|
|
|
- <TableCell className="px-4 py-3 text-gray-600">
|
|
|
|
|
- {record.startTime || '-'}
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell className="px-4 py-3 text-gray-600">
|
|
|
|
|
- {record.endTime || '-'}
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell className="px-4 py-3">
|
|
|
|
|
- <span className={`inline-flex px-3 py-1 rounded-full text-xs font-medium ${getFlowRecordStatusClassName(record.taskStatus)}`}>
|
|
|
|
|
- {getFlowRecordStatusText(record.taskStatus)}
|
|
|
|
|
- </span>
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell className="px-4 py-3">
|
|
|
|
|
- {record.executionDescription ? (
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={() => handleViewForm(record)}
|
|
|
|
|
- className="text-blue-600 hover:text-blue-700 hover:underline text-sm"
|
|
|
|
|
- >
|
|
|
|
|
- {record.executionDescription}
|
|
|
|
|
- </button>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span className="text-gray-400">-</span>
|
|
|
|
|
- )}
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- <TableCell className="px-4 py-3 text-gray-600">
|
|
|
|
|
- {record.duration || '-'}
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- </TableRow>
|
|
|
|
|
- ))
|
|
|
|
|
- )}
|
|
|
|
|
- </TableBody>
|
|
|
|
|
- </Table>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- ),
|
|
|
|
|
- },
|
|
|
|
|
- ]}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- {/* 底部操作按钮 */}
|
|
|
|
|
- <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
|
|
|
|
- <div className="flex items-center gap-3 flex-wrap">
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="default"
|
|
|
|
|
- className="bg-green-600 hover:bg-green-700 text-white"
|
|
|
|
|
- >
|
|
|
|
|
- <Play className="w-4 h-4" />
|
|
|
|
|
- 执行
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="default"
|
|
|
|
|
- className="bg-blue-600 hover:bg-blue-700 text-white"
|
|
|
|
|
- >
|
|
|
|
|
- <Check className="w-4 h-4" />
|
|
|
|
|
- 完成
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- className="border-orange-300 text-orange-600 hover:bg-orange-50"
|
|
|
|
|
- >
|
|
|
|
|
- <Pause className="w-4 h-4" />
|
|
|
|
|
- 暂停
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button variant="outline" className="border-gray-300 text-gray-600 hover:bg-gray-50">
|
|
|
|
|
- <Send className="w-4 h-4" />
|
|
|
|
|
- 抄送
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button variant="outline" className="border-gray-300 text-gray-600 hover:bg-gray-50">
|
|
|
|
|
- <ArrowLeftRight className="w-4 h-4" />
|
|
|
|
|
- 转办
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button variant="outline" className="border-gray-300 text-gray-600 hover:bg-gray-50">
|
|
|
|
|
- <UserPlus className="w-4 h-4" />
|
|
|
|
|
- 委派
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button variant="outline" className="border-gray-300 text-gray-600 hover:bg-gray-50">
|
|
|
|
|
- <Plus className="w-4 h-4" />
|
|
|
|
|
- 加签
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button variant="outline" className="border-gray-300 text-gray-600 hover:bg-gray-50">
|
|
|
|
|
- <ArrowLeft className="w-4 h-4" />
|
|
|
|
|
- 退回
|
|
|
|
|
- </Button>
|
|
|
|
|
- <Button variant="outline" className="border-gray-300 text-gray-600 hover:bg-gray-50">
|
|
|
|
|
- <X className="w-4 h-4" />
|
|
|
|
|
- 取消
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- {/* 版权信息 */}
|
|
|
|
|
- <div className="mt-6 text-center text-sm text-gray-500">
|
|
|
|
|
- Copyright ©2025 锁控管理系统
|
|
|
|
|
- </div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|