Просмотр исходного кода

流程设计器复杂流程测试 修复连接线和节点样式问题

pm 5 месяцев назад
Родитель
Сommit
590a6669b0
2 измененных файлов с 202 добавлено и 47 удалено
  1. 181 17
      src/components/ProcessDesigner.tsx
  2. 21 30
      src/utils/axios.ts

+ 181 - 17
src/components/ProcessDesigner.tsx

@@ -15,6 +15,10 @@ import ReactFlow, {
   Handle,
   Position,
   ConnectionMode,
+  EdgeLabelRenderer,
+  BaseEdge,
+  getStraightPath,
+  EdgeTypes,
 } from 'reactflow';
 import 'reactflow/dist/style.css';
 import {
@@ -441,7 +445,7 @@ function CustomNode({ data, selected, id }: any) {
   
   return (
     <div
-      className={`relative px-3 py-2.5 rounded-lg shadow-sm border-2 w-auto min-w-[100px] max-w-[200px] bg-white ${
+      className={`relative px-4 py-4 rounded-lg shadow-sm border-2 w-[180px] h-auto min-h-[140px] bg-white ${
         selected
           ? 'border-blue-500 shadow-md ring-1 ring-blue-200'
           : 'border-gray-200 hover:border-gray-300'
@@ -513,20 +517,22 @@ function CustomNode({ data, selected, id }: any) {
         isConnectable={true}
       />
       
-      <div className="flex items-center gap-2">
-        <div className={`${config?.bgColor || 'bg-gray-50'} ${config?.borderColor || 'border-gray-100'} border p-2 rounded-xl flex-shrink-0 shadow-sm flex items-center justify-center`} style={{ borderRadius: '12px' }}>
+      {/* 垂直布局:顶部图标、中间名称、底部ID */}
+      <div className="flex flex-col items-center justify-between gap-3 h-full">
+        {/* 顶部:图标 */}
+        <div className={`${config?.bgColor || 'bg-gray-50'} ${config?.borderColor || 'border-gray-100'} border p-3 rounded-xl shadow-sm flex items-center justify-center flex-shrink-0`} style={{ borderRadius: '12px' }}>
           <Icon 
-            className={`${config?.iconColor || 'text-gray-600'} text-base`} 
+            className={`${config?.iconColor || 'text-gray-600'} text-2xl`} 
             style={{ color: config?.iconColorCustom || undefined }}
           />
         </div>
-        <div className="flex-1 min-w-0">
-          <div className="font-semibold text-xs text-gray-900 mb-0.5 leading-tight whitespace-nowrap">
-            {data.label || config?.label}
-          </div>
-          <div className="text-[10px] text-gray-500">
-            ID: {nodeId}
-          </div>
+        {/* 中间:节点名称 */}
+        <div className="font-semibold text-sm text-gray-900 leading-tight text-center break-words w-full flex-1 flex items-center justify-center px-1">
+          {data.label || config?.label}
+        </div>
+        {/* 底部:ID */}
+        <div className="text-xs text-gray-500 text-center flex-shrink-0">
+          ID: {nodeId}
         </div>
       </div>
     </div>
@@ -578,6 +584,87 @@ const nodeTypes = {
   complete: CompleteNode,
 };
 
+// 全局变量存储删除函数(用于边组件)
+let globalDeleteEdgeFn: ((id: string) => void) | null = null;
+
+// 自定义边组件 - 定义在组件外部以确保稳定引用
+function CustomEdgeWithDelete({
+  id,
+  sourceX,
+  sourceY,
+  targetX,
+  targetY,
+  selected,
+  markerEnd,
+  style,
+}: any) {
+  const [edgePath, labelX, labelY] = getStraightPath({
+    sourceX,
+    sourceY,
+    targetX,
+    targetY,
+  });
+
+  console.log('边组件渲染,ID:', id, 'selected:', selected, 'labelX:', labelX, 'labelY:', labelY);
+
+  return (
+    <>
+      <BaseEdge
+        id={id}
+        path={edgePath}
+        markerEnd={markerEnd}
+        style={{
+          ...style,
+          strokeWidth: selected ? 3 : 2,
+          stroke: selected ? '#3b82f6' : '#94a3b8',
+        }}
+      />
+      {selected && (
+        <EdgeLabelRenderer>
+          <div
+            style={{
+              position: 'absolute',
+              left: labelX,
+              top: labelY,
+              transform: 'translate(-50%, -50%)',
+              pointerEvents: 'all',
+              zIndex: 1000,
+            }}
+            className="nodrag nopan"
+          >
+            <button
+              onClick={(e) => {
+                e.stopPropagation();
+                e.preventDefault();
+                console.log('删除按钮被点击,连线ID:', id);
+                if (globalDeleteEdgeFn) {
+                  globalDeleteEdgeFn(id);
+                }
+              }}
+              onMouseDown={(e) => {
+                e.stopPropagation();
+              }}
+              className="bg-red-500 hover:bg-red-600 rounded-full p-1.5 shadow-lg transition-colors flex items-center justify-center w-8 h-8"
+              title="删除连线"
+              type="button"
+              style={{ cursor: 'pointer' }}
+            >
+              <DeleteOutlined className="text-sm text-white" style={{ color: 'white' }} />
+            </button>
+          </div>
+        </EdgeLabelRenderer>
+      )}
+    </>
+  );
+}
+
+// 自定义边类型映射 - 定义在组件外部
+const edgeTypes: EdgeTypes = {
+  straight: CustomEdgeWithDelete,
+  default: CustomEdgeWithDelete,
+};
+
+
 export default function ProcessDesigner() {
   const navigate = useNavigate();
   const location = useLocation();
@@ -591,6 +678,7 @@ export default function ProcessDesigner() {
     return id ? parseInt(id, 10) : null;
   }, [location.search]);
   const [selectedNode, setSelectedNode] = useState<Node | null>(null);
+  const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null);
   const [activeTabKey, setActiveTabKey] = useState<string>('info');
   const reactFlowWrapper = useRef<HTMLDivElement>(null);
   const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);
@@ -820,7 +908,7 @@ export default function ProcessDesigner() {
           target: targetId,
           sourceHandle: edge.sourceHandle,
           targetHandle: edge.targetHandle,
-          type: edge.type || 'smoothstep',
+          type: edge.type || 'straight',
           animated: false,
           style: { strokeWidth: 2, stroke: '#94a3b8' },
         };
@@ -978,7 +1066,7 @@ export default function ProcessDesigner() {
           targetHandle: targetHandle || undefined,
           animated: false,
           style: { strokeWidth: 2, stroke: '#94a3b8' },
-          type: 'smoothstep',
+          type: 'straight',
         };
         // 直接添加到数组,不使用 addEdge(因为 addEdge 可能会过滤掉某些连接)
         const newEdges = [...eds, newEdge];
@@ -1063,7 +1151,15 @@ export default function ProcessDesigner() {
   // 节点点击处理
   const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
     setSelectedNode(node);
+    setSelectedEdge(null); // 点击节点时取消连线选择
     setActiveTabKey('info'); // 切换节点时重置到第一个tab
+    // 清除所有边的选中状态
+    setEdges((eds) =>
+      eds.map((e) => ({
+        ...e,
+        selected: false,
+      }))
+    );
     // 加载节点配置
     const nodeData = node.data || {};
     const cache = loadNodeCache(node.id);
@@ -1112,12 +1208,78 @@ export default function ProcessDesigner() {
       notificationPerson: source.notificationPerson || '',
       notificationTime: source.notificationTime || '',
     });
-  }, [loadNodeCache, nodes]);
+  }, [loadNodeCache, nodes, setEdges]);
 
   // 画布点击处理(取消选择)
   const onPaneClick = useCallback(() => {
     setSelectedNode(null);
-  }, []);
+    setSelectedEdge(null);
+    // 清除所有边的选中状态
+    setEdges((eds) =>
+      eds.map((e) => ({
+        ...e,
+        selected: false,
+      }))
+    );
+  }, [setEdges]);
+
+  // 连线点击处理
+  const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
+    event.stopPropagation();
+    console.log('连线被点击:', edge.id, '当前type:', edge.type);
+    setSelectedEdge(edge);
+    setSelectedNode(null); // 点击连线时取消节点选择
+    // 更新边的选中状态,并确保类型是 straight
+    setEdges((eds) => {
+      const updatedEdges = eds.map((e) => ({
+        ...e,
+        type: 'straight', // 强制设置为 straight 类型
+        selected: e.id === edge.id,
+      }));
+      const clickedEdge = updatedEdges.find(e => e.id === edge.id);
+      console.log('更新后的边状态:', clickedEdge?.selected, 'type:', clickedEdge?.type);
+      return updatedEdges;
+    });
+  }, [setEdges]);
+
+  // 删除连线
+  const handleDeleteEdge = useCallback((edgeId: string) => {
+    console.log('删除连线:', edgeId);
+    setEdges((eds) => {
+      const newEdges = eds.filter((edge) => edge.id !== edgeId);
+      console.log('删除后的连线数量:', newEdges.length);
+      // 保存历史
+      const newHistory = history.slice(0, historyIndex + 1);
+      newHistory.push({ nodes: [...nodes], edges: [...newEdges] });
+      setHistory(newHistory);
+      setHistoryIndex(newHistory.length - 1);
+      return newEdges;
+    });
+    setSelectedEdge(null);
+  }, [history, historyIndex, nodes]);
+
+  // 更新全局删除函数
+  useEffect(() => {
+    globalDeleteEdgeFn = handleDeleteEdge;
+    return () => {
+      globalDeleteEdgeFn = null;
+    };
+  }, [handleDeleteEdge]);
+
+  // 确保所有边都使用 'straight' 类型(用于自定义边组件)
+  useEffect(() => {
+    setEdges((eds) => {
+      const needsUpdate = eds.some(e => e.type !== 'straight');
+      if (needsUpdate) {
+        console.log('统一设置所有边的类型为 straight');
+        return eds.map(e => ({
+          ...e,
+          type: 'straight',
+        }));
+      }
+      return eds;
+    });
+  }, []); // 只在组件挂载时执行一次
 
   // 删除节点
   const handleDeleteNode = useCallback((nodeId?: string) => {
@@ -1621,7 +1783,7 @@ export default function ProcessDesigner() {
           target: targetId,
           sourceHandle: edge.sourceHandle,
           targetHandle: edge.targetHandle,
-          type: edge.type || 'smoothstep',
+          type: edge.type || 'straight',
           animated: false,
           style: { strokeWidth: 2, stroke: '#94a3b8' },
         };
@@ -1822,18 +1984,20 @@ export default function ProcessDesigner() {
             isValidConnection={isValidConnection}
             onNodeClick={onNodeClick}
             onNodeContextMenu={onNodeContextMenu}
+            onEdgeClick={onEdgeClick}
             onPaneClick={onPaneClick}
             onDrop={onDrop}
             onDragOver={onDragOver}
             deleteKeyCode={['Delete', 'Backspace']}
             nodeTypes={nodeTypes as NodeTypes}
+            edgeTypes={edgeTypes}
             fitView
             onInit={setReactFlowInstance}
             onMove={(_, viewport) => setZoom(viewport.zoom)}
             connectionMode={ConnectionMode.Loose}
             defaultEdgeOptions={{
               style: { strokeWidth: 2, stroke: '#94a3b8' },
-              type: 'smoothstep',
+              type: 'straight',
             }}
             edgesUpdatable={true}
             edgesFocusable={true}

+ 21 - 30
src/utils/axios.ts

@@ -77,11 +77,10 @@ axiosInstance.interceptors.response.use(
         // 业务错误码 401,表示未登录,需要处理 token 刷新或退出登录
         const originalRequest = config as InternalAxiosRequestConfig & { _retry?: boolean };
         
-        // 如果是刷新token的请求失败,直接跳转登录
+        // 如果是刷新token的请求失败,不退出登录,继续保留当前页面
         if (originalRequest.url?.includes('/refresh-token') || originalRequest._retry) {
-          const errorMessage = '登录已过期,请重新登录';
-          clearAuth();
-          window.location.href = '/login';
+          const errorMessage = 'Token刷新失败,请稍后重试';
+          // 不清除认证信息,不跳转登录,继续保留当前页面
           return Promise.reject(new Error(errorMessage));
         }
 
@@ -104,9 +103,8 @@ axiosInstance.interceptors.response.use(
         // 尝试刷新token
         const refreshToken = getRefreshToken();
         if (!refreshToken) {
-          const errorMessage = '登录已过期,请重新登录';
-          clearAuth();
-          window.location.href = '/login';
+          const errorMessage = 'RefreshToken不存在,无法刷新';
+          // 不清除认证信息,不跳转登录,继续保留当前页面
           return Promise.reject(new Error(errorMessage));
         }
 
@@ -154,11 +152,10 @@ axiosInstance.interceptors.response.use(
             }
           })
           .catch((refreshError: any) => {
-            // 刷新失败,清除所有认证信息并跳转登录
-            const errorMessage = '登录已过期,请重新登录';
+            // 刷新失败,不清除认证信息,不跳转登录,继续保留当前页面
+            const errorMessage = refreshError?.message || 'Token刷新失败,请稍后重试';
             processQueue(refreshError, null);
-            clearAuth();
-            window.location.href = '/login';
+            // 不清除认证信息,不跳转登录
             return Promise.reject(new Error(errorMessage));
           })
           .finally(() => {
@@ -178,10 +175,9 @@ axiosInstance.interceptors.response.use(
         if (data.code === 401) {
           const originalRequest = config as InternalAxiosRequestConfig & { _retry?: boolean };
           
-          // 如果是刷新token的请求失败,直接跳转登录
+          // 如果是刷新token的请求失败,不退出登录,继续保留当前页面
           if (originalRequest.url?.includes('/refresh-token') || originalRequest._retry) {
-            clearAuth();
-            window.location.href = '/login';
+            // 不清除认证信息,不跳转登录,继续保留当前页面
             return Promise.reject(error);
           }
 
@@ -204,8 +200,7 @@ axiosInstance.interceptors.response.use(
           // 尝试刷新token
           const refreshToken = getRefreshToken();
           if (!refreshToken) {
-            clearAuth();
-            window.location.href = '/login';
+            // 不清除认证信息,不跳转登录,继续保留当前页面
             return Promise.reject(error);
           }
 
@@ -253,10 +248,9 @@ axiosInstance.interceptors.response.use(
               }
             })
             .catch((refreshError: any) => {
-              // 刷新失败,清除所有认证信息并跳转登录
+              // 刷新失败,不清除认证信息,不跳转登录,继续保留当前页面
               processQueue(refreshError, null);
-              clearAuth();
-              window.location.href = '/login';
+              // 不清除认证信息,不跳转登录
               return Promise.reject(error);
             })
             .finally(() => {
@@ -284,11 +278,10 @@ axiosInstance.interceptors.response.use(
           // 获取原始请求配置
           const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
           
-          // 如果是刷新token的请求失败,直接跳转登录
+          // 如果是刷新token的请求失败,不退出登录,继续保留当前页面
           if (originalRequest.url?.includes('/refresh-token') || originalRequest._retry) {
-            errorMessage = '登录已过期,请重新登录';
-            clearAuth();
-            window.location.href = '/login';
+            errorMessage = 'Token刷新失败,请稍后重试';
+            // 不清除认证信息,不跳转登录,继续保留当前页面
             return Promise.reject(new Error(errorMessage));
           }
 
@@ -311,9 +304,8 @@ axiosInstance.interceptors.response.use(
           // 尝试刷新token
           const refreshToken = getRefreshToken();
           if (!refreshToken) {
-            errorMessage = '登录已过期,请重新登录';
-            clearAuth();
-            window.location.href = '/login';
+            errorMessage = 'RefreshToken不存在,无法刷新';
+            // 不清除认证信息,不跳转登录,继续保留当前页面
             return Promise.reject(new Error(errorMessage));
           }
 
@@ -361,11 +353,10 @@ axiosInstance.interceptors.response.use(
               }
             })
             .catch((refreshError: any) => {
-              // 刷新失败,清除所有认证信息并跳转登录
-              errorMessage = '登录已过期,请重新登录';
+              // 刷新失败,不清除认证信息,不跳转登录,继续保留当前页面
+              errorMessage = refreshError?.message || 'Token刷新失败,请稍后重试';
               processQueue(refreshError, null);
-              clearAuth();
-              window.location.href = '/login';
+              // 不清除认证信息,不跳转登录
               return Promise.reject(new Error(errorMessage));
             })
             .finally(() => {