From 03814728f1a7e349df5d124ba5bb1bf92b85ac49 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 6 Nov 2025 18:34:57 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=89=8D=E7=AB=AF=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/DeployFlowGraphModal.tsx | 216 ++++++++++++------ .../components/PendingApprovalModal.tsx | 4 +- frontend/src/pages/Dashboard/index.tsx | 23 +- frontend/src/pages/Dashboard/service.ts | 9 +- .../List/components/DeployDialog.tsx | 6 +- .../pages/Workflow/Definition/List/index.tsx | 4 +- .../Design/components/NodeConfigModal.tsx | 6 + 7 files changed, 181 insertions(+), 87 deletions(-) diff --git a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx index 031a0605..8e3aa10e 100644 --- a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx +++ b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { ReactFlowProvider, ReactFlow, Background, Controls, MiniMap, Node, Edge, Handle, Position, BackgroundVariant } from '@xyflow/react'; +import { ReactFlowProvider, ReactFlow, Background, Node, Edge, Handle, Position, BackgroundVariant } from '@xyflow/react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -43,7 +43,7 @@ interface DeployFlowGraphModalProps { * 自定义流程节点组件 */ const CustomFlowNode: React.FC = ({ data }) => { - const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage } = data; + const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage, isUnreachable } = data; const statusColor = getNodeStatusColor(status); const isNotStarted = status === 'NOT_STARTED'; const isRunning = status === 'RUNNING'; @@ -68,7 +68,8 @@ const CustomFlowNode: React.FC = ({ data }) => { 'px-4 py-3 rounded-md min-w-[160px] transition-all', isNotStarted && 'border-2 border-dashed', !isNotStarted && 'border-2 border-solid shadow-sm', - isRunning && 'animate-pulse' + isRunning && 'animate-pulse', + isUnreachable && 'opacity-40' // 不可达节点半透明 )} style={{ borderColor: statusColor, @@ -399,10 +400,41 @@ export const DeployFlowGraphModal: React.FC = ({ return map; }, [flowData]); - // 转换为 React Flow 节点(按执行顺序重新布局) + // 计算可达节点(从已执行节点出发能到达的节点) + const reachableNodes = useMemo(() => { + if (!flowData?.graph?.edges || !flowData?.nodeInstances) return new Set(); + + const executedNodeIds = flowData.nodeInstances + .filter(ni => ni.status !== 'NOT_STARTED') + .map(ni => ni.nodeId); + + if (executedNodeIds.length === 0) return new Set(); + + const reachable = new Set(executedNodeIds); + const queue = [...executedNodeIds]; + + // BFS 遍历所有可达节点 + while (queue.length > 0) { + const currentId = queue.shift()!; + const outgoingEdges = flowData.graph.edges.filter(e => e.from === currentId); + + for (const edge of outgoingEdges) { + if (!reachable.has(edge.to)) { + reachable.add(edge.to); + queue.push(edge.to); + } + } + } + + return reachable; + }, [flowData]); + + // 转换为 React Flow 节点(显示所有节点,但对不可达节点置灰) const flowNodes: Node[] = useMemo(() => { if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return []; + const isRunning = flowData.runningNodeCount > 0; + // 按执行顺序排序已执行的节点 const executedInstances = flowData.nodeInstances .filter(ni => ni.status !== 'NOT_STARTED') @@ -412,12 +444,14 @@ export const DeployFlowGraphModal: React.FC = ({ return timeA - timeB; }); - // 创建节点位置映射(已执行的节点) + // 线性布局:从左到右排列 const nodePositionMap = new Map(); - const horizontalSpacing = 250; - const startX = 50; - const startY = 150; + const horizontalSpacing = 300; // 水平间距 + const verticalSpacing = 150; // 垂直间距(用于分支) + const startX = 100; // 起始X坐标 + const startY = 150; // 起始Y坐标 + // 已执行的节点:从左到右线性排列 executedInstances.forEach((instance, index) => { nodePositionMap.set(instance.nodeId, { x: startX + index * horizontalSpacing, @@ -425,19 +459,31 @@ export const DeployFlowGraphModal: React.FC = ({ }); }); - // 为所有节点生成位置(包括未执行的) + // 未执行的节点:根据在图中的位置和层级布局 + const notStartedNodes = flowData.graph.nodes.filter( + node => !nodePositionMap.has(node.id) + ); + + // 简单布局:按节点在edges中的出现顺序,从左到右、上到下排列 + let currentX = startX + executedInstances.length * horizontalSpacing; + let currentRow = 0; + const nodesPerRow = 3; + + notStartedNodes.forEach((node, index) => { + const row = Math.floor(index / nodesPerRow); + const col = index % nodesPerRow; + nodePositionMap.set(node.id, { + x: currentX + col * horizontalSpacing, + y: startY + row * verticalSpacing, + }); + }); + + // 生成所有节点 return flowData.graph.nodes.map((node) => { const instance = nodeInstanceMap.get(node.id); - - // 如果节点已执行,使用计算的位置;否则使用原始位置但偏移到下方 - let position = nodePositionMap.get(node.id); - if (!position) { - // 未执行的节点放在下方,使用原始相对位置 - position = { - x: node.position.x + 500, - y: node.position.y + 400, - }; - } + const position = nodePositionMap.get(node.id) || { x: 0, y: 0 }; + const isReachable = reachableNodes.has(node.id); + const isNotStarted = !instance || instance.status === 'NOT_STARTED'; return { id: node.id, @@ -451,51 +497,90 @@ export const DeployFlowGraphModal: React.FC = ({ endTime: instance?.endTime, duration: instance?.duration, errorMessage: instance?.errorMessage, + // 新增:不可达且未执行的节点标记为置灰 + isUnreachable: isRunning && isNotStarted && !isReachable, }, }; }); - }, [flowData, nodeInstanceMap]); + }, [flowData, nodeInstanceMap, reachableNodes]); // 转换为 React Flow 边 const flowEdges: Edge[] = useMemo(() => { if (!flowData?.graph?.edges) return []; - return flowData.graph.edges.map((edge, index) => { - const source = edge.from; - const target = edge.to; - const sourceInstance = nodeInstanceMap.get(source); - const sourceStatus = sourceInstance?.status || 'NOT_STARTED'; - - // 根据源节点状态确定边的颜色 - let strokeColor = '#d1d5db'; // 默认灰色 - if (sourceStatus === 'COMPLETED') { - strokeColor = '#10b981'; // 绿色 - } else if (sourceStatus === 'FAILED') { - strokeColor = '#ef4444'; // 红色 - } else if (sourceStatus === 'RUNNING') { - strokeColor = '#3b82f6'; // 蓝色 - } + const isRunning = flowData.runningNodeCount > 0; + const displayedNodeIds = new Set(flowNodes.map(n => n.id)); - return { - id: edge.id || `edge-${source}-${target}-${index}`, - source, - target, - type: 'smoothstep', - // 不显示边的标签(条件表达式等) - animated: sourceStatus === 'RUNNING', - style: { - stroke: strokeColor, - strokeWidth: 2, - }, - markerEnd: { - type: 'arrowclosed', - color: strokeColor, - width: 20, - height: 20, - }, - }; - }); - }, [flowData, nodeInstanceMap]); + return flowData.graph.edges + .filter(edge => { + // 只显示连接已显示节点的边 + return displayedNodeIds.has(edge.from) && displayedNodeIds.has(edge.to); + }) + .map((edge, index) => { + const source = edge.from; + const target = edge.to; + const sourceInstance = nodeInstanceMap.get(source); + const targetInstance = nodeInstanceMap.get(target); + const sourceStatus = sourceInstance?.status || 'NOT_STARTED'; + const targetStatus = targetInstance?.status || 'NOT_STARTED'; + + // 判断这条边是否在可达路径上 + const isReachableEdge = reachableNodes.has(source) && reachableNodes.has(target); + + // 根据节点状态确定边的样式 + let strokeColor = '#d1d5db'; // 默认灰色 + let strokeWidth = 2; + let animated = false; + let opacity = 1; + + // 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线 + if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) { + strokeColor = '#10b981'; // 绿色 + } + // 源节点失败 = 红色 + else if (sourceStatus === 'FAILED') { + strokeColor = '#ef4444'; // 红色 + } + // 源节点运行中 = 蓝色动画 + else if (sourceStatus === 'RUNNING') { + strokeColor = '#3b82f6'; // 蓝色 + animated = true; + } + // 源节点完成 + 目标节点未开始 + else if (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED') { + if (isReachableEdge && isRunning) { + strokeColor = '#9ca3af'; // 浅灰色(即将执行) + } else { + strokeColor = '#d1d5db'; // 默认灰色 + opacity = 0.3; // 不可达路径半透明 + } + } + // 两端都未执行 + else if (sourceStatus === 'NOT_STARTED' && targetStatus === 'NOT_STARTED') { + opacity = isRunning && !isReachableEdge ? 0.3 : 0.5; + } + + return { + id: edge.id || `edge-${source}-${target}-${index}`, + source, + target, + type: 'smoothstep', + animated, + style: { + stroke: strokeColor, + strokeWidth, + opacity, + strokeDasharray: (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED' && isReachableEdge) ? '5,5' : undefined, + }, + markerEnd: { + type: 'arrowclosed', + color: strokeColor, + width: 20, + height: 20, + }, + }; + }); + }, [flowData, nodeInstanceMap, flowNodes, reachableNodes]); // 获取部署状态信息 const deployStatusInfo = flowData @@ -559,7 +644,11 @@ export const DeployFlowGraphModal: React.FC = ({ nodeTypes={nodeTypes} fitView className="bg-muted/10" - fitViewOptions={{ padding: 0.15, maxZoom: 1.2 }} + fitViewOptions={{ padding: 0.2, maxZoom: 1, minZoom: 0.5 }} + panOnScroll={true} + zoomOnScroll={true} + zoomOnPinch={true} + preventScrolling={false} > = ({ size={1} className="opacity-30" /> - - { - const status = node.data?.status || 'NOT_STARTED'; - return getNodeStatusColor(status); - }} - className="bg-background/80 backdrop-blur-sm border shadow-sm" - pannable={true} - zoomable={true} - /> diff --git a/frontend/src/pages/Dashboard/components/PendingApprovalModal.tsx b/frontend/src/pages/Dashboard/components/PendingApprovalModal.tsx index 81d5657d..6fa3b313 100644 --- a/frontend/src/pages/Dashboard/components/PendingApprovalModal.tsx +++ b/frontend/src/pages/Dashboard/components/PendingApprovalModal.tsx @@ -38,11 +38,13 @@ import { DeployFlowGraphModal } from './DeployFlowGraphModal'; interface PendingApprovalModalProps { open: boolean; onOpenChange: (open: boolean) => void; + workflowDefinitionKeys?: string[]; // 工作流定义键列表 } export const PendingApprovalModal: React.FC = ({ open, onOpenChange, + workflowDefinitionKeys, }) => { const { toast } = useToast(); const [loading, setLoading] = useState(false); @@ -59,7 +61,7 @@ export const PendingApprovalModal: React.FC = ({ const loadApprovalList = async () => { try { setLoading(true); - const response = await getMyApprovalTasks(); + const response = await getMyApprovalTasks(workflowDefinitionKeys); if (response) { setApprovalList(response || []); } diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx index acfce387..3cdbd3a0 100644 --- a/frontend/src/pages/Dashboard/index.tsx +++ b/frontend/src/pages/Dashboard/index.tsx @@ -56,10 +56,22 @@ const Dashboard: React.FC = () => { // 从 Redux store 中获取当前登录用户信息 const currentUserId = useSelector((state: RootState) => state.user.userInfo?.id); + // 提取所有工作流定义键(去重) + const workflowDefinitionKeys = React.useMemo(() => { + const workflowKeys = teams.flatMap(team => + team.environments.flatMap(env => + env.applications + .map(app => app.workflowDefinitionKey) + .filter((key): key is string => !!key) + ) + ); + return Array.from(new Set(workflowKeys)); + }, [teams]); + // 加载待审批数量 const loadPendingApprovalCount = React.useCallback(async () => { try { - const response = await getMyApprovalTasks(); + const response = await getMyApprovalTasks(workflowDefinitionKeys); if (response) { setPendingApprovalCount(response.length || 0); } @@ -67,7 +79,7 @@ const Dashboard: React.FC = () => { // 静默失败,不影响主页面 console.error('Failed to load pending approval count:', error); } - }, []); + }, [workflowDefinitionKeys]); // 加载部署环境数据 const loadData = React.useCallback(async (showLoading = false) => { @@ -324,12 +336,6 @@ const Dashboard: React.FC = () => { {currentTeam && (
{currentTeam.teamName} - {currentTeam.teamRole && ( - <> - · - {currentTeam.teamRole} - - )} {currentTeam.description && ( <> · @@ -453,6 +459,7 @@ const Dashboard: React.FC = () => {
); diff --git a/frontend/src/pages/Dashboard/service.ts b/frontend/src/pages/Dashboard/service.ts index c97121d2..a69248c1 100644 --- a/frontend/src/pages/Dashboard/service.ts +++ b/frontend/src/pages/Dashboard/service.ts @@ -25,9 +25,14 @@ export const getDeployRecordFlowGraph = (deployRecordId: number) => /** * 获取我的待审批任务列表 + * @param workflowDefinitionKeys 工作流定义键列表(可选) */ -export const getMyApprovalTasks = () => - request.get(`${DEPLOY_URL}/my-approval-tasks`); +export const getMyApprovalTasks = (workflowDefinitionKeys?: string[]) => { + const params = workflowDefinitionKeys && workflowDefinitionKeys.length > 0 + ? { workflowDefinitionKeys: workflowDefinitionKeys.join(',') } + : {}; + return request.get(`${DEPLOY_URL}/my-approval-tasks`, { params }); +}; /** * 完成审批 diff --git a/frontend/src/pages/Workflow/Definition/List/components/DeployDialog.tsx b/frontend/src/pages/Workflow/Definition/List/components/DeployDialog.tsx index dc7471b2..45f213f5 100644 --- a/frontend/src/pages/Workflow/Definition/List/components/DeployDialog.tsx +++ b/frontend/src/pages/Workflow/Definition/List/components/DeployDialog.tsx @@ -9,7 +9,7 @@ import { } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; -import { CheckCircle2, FileText, Loader2 } from 'lucide-react'; +import { CheckCircle2, FileText, Loader2, Rocket } from 'lucide-react'; import type { WorkflowDefinition } from '../types'; import { getDefinitionById as getFormDefinitionById } from '@/pages/Form/Definition/List/service'; import type { FormDefinitionResponse } from '@/pages/Form/Definition/List/types'; @@ -56,7 +56,7 @@ const DeployDialog: React.FC = ({ - 确认发布工作流? + 确认发布工作流? 您确定要发布工作流 "{record.name}" 吗? @@ -104,7 +104,7 @@ const DeployDialog: React.FC = ({ 取消 diff --git a/frontend/src/pages/Workflow/Definition/List/index.tsx b/frontend/src/pages/Workflow/Definition/List/index.tsx index c130c616..6e066b07 100644 --- a/frontend/src/pages/Workflow/Definition/List/index.tsx +++ b/frontend/src/pages/Workflow/Definition/List/index.tsx @@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DataTablePagination } from '@/components/ui/pagination'; import { - Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2, + Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2, Rocket, Clock, Activity, Workflow, Eye, Pencil, FolderKanban } from 'lucide-react'; import { useToast } from '@/components/ui/use-toast'; @@ -486,7 +486,7 @@ const WorkflowDefinitionList: React.FC = () => { title="发布" className="text-green-600 hover:text-green-700 hover:bg-green-50" > - + ) : ( diff --git a/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx b/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx index 3045a258..8b07bf7d 100644 --- a/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx +++ b/frontend/src/pages/Workflow/Design/components/NodeConfigModal.tsx @@ -371,6 +371,12 @@ const NodeConfigModal: React.FC = ({ return Array.isArray(currentValue) && currentValue.includes(expectedValue); case 'notIncludes': return Array.isArray(currentValue) && !currentValue.includes(expectedValue); + case 'in': + // 检查 currentValue 是否在 expectedValue 数组中 + return Array.isArray(expectedValue) && expectedValue.includes(currentValue); + case 'notIn': + // 检查 currentValue 是否不在 expectedValue 数组中 + return Array.isArray(expectedValue) && !expectedValue.includes(currentValue); default: return currentValue === expectedValue; }