重构前端逻辑

This commit is contained in:
dengqichen 2025-11-06 18:34:57 +08:00
parent 9acae67438
commit 03814728f1
7 changed files with 181 additions and 87 deletions

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from 'react'; 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 { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -43,7 +43,7 @@ interface DeployFlowGraphModalProps {
* *
*/ */
const CustomFlowNode: React.FC<any> = ({ data }) => { const CustomFlowNode: React.FC<any> = ({ 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 statusColor = getNodeStatusColor(status);
const isNotStarted = status === 'NOT_STARTED'; const isNotStarted = status === 'NOT_STARTED';
const isRunning = status === 'RUNNING'; const isRunning = status === 'RUNNING';
@ -68,7 +68,8 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
'px-4 py-3 rounded-md min-w-[160px] transition-all', 'px-4 py-3 rounded-md min-w-[160px] transition-all',
isNotStarted && 'border-2 border-dashed', isNotStarted && 'border-2 border-dashed',
!isNotStarted && 'border-2 border-solid shadow-sm', !isNotStarted && 'border-2 border-solid shadow-sm',
isRunning && 'animate-pulse' isRunning && 'animate-pulse',
isUnreachable && 'opacity-40' // 不可达节点半透明
)} )}
style={{ style={{
borderColor: statusColor, borderColor: statusColor,
@ -399,10 +400,41 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
return map; return map;
}, [flowData]); }, [flowData]);
// 转换为 React Flow 节点(按执行顺序重新布局) // 计算可达节点(从已执行节点出发能到达的节点)
const reachableNodes = useMemo(() => {
if (!flowData?.graph?.edges || !flowData?.nodeInstances) return new Set<string>();
const executedNodeIds = flowData.nodeInstances
.filter(ni => ni.status !== 'NOT_STARTED')
.map(ni => ni.nodeId);
if (executedNodeIds.length === 0) return new Set<string>();
const reachable = new Set<string>(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(() => { const flowNodes: Node[] = useMemo(() => {
if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return []; if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return [];
const isRunning = flowData.runningNodeCount > 0;
// 按执行顺序排序已执行的节点 // 按执行顺序排序已执行的节点
const executedInstances = flowData.nodeInstances const executedInstances = flowData.nodeInstances
.filter(ni => ni.status !== 'NOT_STARTED') .filter(ni => ni.status !== 'NOT_STARTED')
@ -412,12 +444,14 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
return timeA - timeB; return timeA - timeB;
}); });
// 创建节点位置映射(已执行的节点) // 线性布局:从左到右排列
const nodePositionMap = new Map<string, { x: number; y: number }>(); const nodePositionMap = new Map<string, { x: number; y: number }>();
const horizontalSpacing = 250; const horizontalSpacing = 300; // 水平间距
const startX = 50; const verticalSpacing = 150; // 垂直间距(用于分支)
const startY = 150; const startX = 100; // 起始X坐标
const startY = 150; // 起始Y坐标
// 已执行的节点:从左到右线性排列
executedInstances.forEach((instance, index) => { executedInstances.forEach((instance, index) => {
nodePositionMap.set(instance.nodeId, { nodePositionMap.set(instance.nodeId, {
x: startX + index * horizontalSpacing, x: startX + index * horizontalSpacing,
@ -425,19 +459,31 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
}); });
}); });
// 为所有节点生成位置(包括未执行的) // 未执行的节点:根据在图中的位置和层级布局
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) => { return flowData.graph.nodes.map((node) => {
const instance = nodeInstanceMap.get(node.id); const instance = nodeInstanceMap.get(node.id);
const position = nodePositionMap.get(node.id) || { x: 0, y: 0 };
// 如果节点已执行,使用计算的位置;否则使用原始位置但偏移到下方 const isReachable = reachableNodes.has(node.id);
let position = nodePositionMap.get(node.id); const isNotStarted = !instance || instance.status === 'NOT_STARTED';
if (!position) {
// 未执行的节点放在下方,使用原始相对位置
position = {
x: node.position.x + 500,
y: node.position.y + 400,
};
}
return { return {
id: node.id, id: node.id,
@ -451,51 +497,90 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
endTime: instance?.endTime, endTime: instance?.endTime,
duration: instance?.duration, duration: instance?.duration,
errorMessage: instance?.errorMessage, errorMessage: instance?.errorMessage,
// 新增:不可达且未执行的节点标记为置灰
isUnreachable: isRunning && isNotStarted && !isReachable,
}, },
}; };
}); });
}, [flowData, nodeInstanceMap]); }, [flowData, nodeInstanceMap, reachableNodes]);
// 转换为 React Flow 边 // 转换为 React Flow 边
const flowEdges: Edge[] = useMemo(() => { const flowEdges: Edge[] = useMemo(() => {
if (!flowData?.graph?.edges) return []; if (!flowData?.graph?.edges) return [];
return flowData.graph.edges.map((edge, index) => { const isRunning = flowData.runningNodeCount > 0;
const source = edge.from; const displayedNodeIds = new Set(flowNodes.map(n => n.id));
const target = edge.to;
const sourceInstance = nodeInstanceMap.get(source);
const sourceStatus = sourceInstance?.status || 'NOT_STARTED';
// 根据源节点状态确定边的颜色 return flowData.graph.edges
let strokeColor = '#d1d5db'; // 默认灰色 .filter(edge => {
if (sourceStatus === 'COMPLETED') { // 只显示连接已显示节点的边
strokeColor = '#10b981'; // 绿色 return displayedNodeIds.has(edge.from) && displayedNodeIds.has(edge.to);
} else if (sourceStatus === 'FAILED') { })
strokeColor = '#ef4444'; // 红色 .map((edge, index) => {
} else if (sourceStatus === 'RUNNING') { const source = edge.from;
strokeColor = '#3b82f6'; // 蓝色 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';
return { // 判断这条边是否在可达路径上
id: edge.id || `edge-${source}-${target}-${index}`, const isReachableEdge = reachableNodes.has(source) && reachableNodes.has(target);
source,
target, // 根据节点状态确定边的样式
type: 'smoothstep', let strokeColor = '#d1d5db'; // 默认灰色
// 不显示边的标签(条件表达式等) let strokeWidth = 2;
animated: sourceStatus === 'RUNNING', let animated = false;
style: { let opacity = 1;
stroke: strokeColor,
strokeWidth: 2, // 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线
}, if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) {
markerEnd: { strokeColor = '#10b981'; // 绿色
type: 'arrowclosed', }
color: strokeColor, // 源节点失败 = 红色
width: 20, else if (sourceStatus === 'FAILED') {
height: 20, strokeColor = '#ef4444'; // 红色
}, }
}; // 源节点运行中 = 蓝色动画
}); else if (sourceStatus === 'RUNNING') {
}, [flowData, nodeInstanceMap]); 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 const deployStatusInfo = flowData
@ -559,7 +644,11 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
fitView fitView
className="bg-muted/10" 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}
> >
<Background <Background
variant={BackgroundVariant.Dots} variant={BackgroundVariant.Dots}
@ -567,21 +656,6 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
size={1} size={1}
className="opacity-30" className="opacity-30"
/> />
<Controls
className="bg-background/80 backdrop-blur-sm border shadow-sm"
showZoom={true}
showFitView={true}
showInteractive={true}
/>
<MiniMap
nodeColor={(node: any) => {
const status = node.data?.status || 'NOT_STARTED';
return getNodeStatusColor(status);
}}
className="bg-background/80 backdrop-blur-sm border shadow-sm"
pannable={true}
zoomable={true}
/>
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>
</div> </div>

View File

@ -38,11 +38,13 @@ import { DeployFlowGraphModal } from './DeployFlowGraphModal';
interface PendingApprovalModalProps { interface PendingApprovalModalProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
workflowDefinitionKeys?: string[]; // 工作流定义键列表
} }
export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
open, open,
onOpenChange, onOpenChange,
workflowDefinitionKeys,
}) => { }) => {
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -59,7 +61,7 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
const loadApprovalList = async () => { const loadApprovalList = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await getMyApprovalTasks(); const response = await getMyApprovalTasks(workflowDefinitionKeys);
if (response) { if (response) {
setApprovalList(response || []); setApprovalList(response || []);
} }

View File

@ -56,10 +56,22 @@ const Dashboard: React.FC = () => {
// 从 Redux store 中获取当前登录用户信息 // 从 Redux store 中获取当前登录用户信息
const currentUserId = useSelector((state: RootState) => state.user.userInfo?.id); 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 () => { const loadPendingApprovalCount = React.useCallback(async () => {
try { try {
const response = await getMyApprovalTasks(); const response = await getMyApprovalTasks(workflowDefinitionKeys);
if (response) { if (response) {
setPendingApprovalCount(response.length || 0); setPendingApprovalCount(response.length || 0);
} }
@ -67,7 +79,7 @@ const Dashboard: React.FC = () => {
// 静默失败,不影响主页面 // 静默失败,不影响主页面
console.error('Failed to load pending approval count:', error); console.error('Failed to load pending approval count:', error);
} }
}, []); }, [workflowDefinitionKeys]);
// 加载部署环境数据 // 加载部署环境数据
const loadData = React.useCallback(async (showLoading = false) => { const loadData = React.useCallback(async (showLoading = false) => {
@ -324,12 +336,6 @@ const Dashboard: React.FC = () => {
{currentTeam && ( {currentTeam && (
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="font-medium text-foreground">{currentTeam.teamName}</span> <span className="font-medium text-foreground">{currentTeam.teamName}</span>
{currentTeam.teamRole && (
<>
<span>·</span>
<span>{currentTeam.teamRole}</span>
</>
)}
{currentTeam.description && ( {currentTeam.description && (
<> <>
<span>·</span> <span>·</span>
@ -453,6 +459,7 @@ const Dashboard: React.FC = () => {
<PendingApprovalModal <PendingApprovalModal
open={approvalModalOpen} open={approvalModalOpen}
onOpenChange={setApprovalModalOpen} onOpenChange={setApprovalModalOpen}
workflowDefinitionKeys={workflowDefinitionKeys}
/> />
</div> </div>
); );

View File

@ -25,9 +25,14 @@ export const getDeployRecordFlowGraph = (deployRecordId: number) =>
/** /**
* *
* @param workflowDefinitionKeys
*/ */
export const getMyApprovalTasks = () => export const getMyApprovalTasks = (workflowDefinitionKeys?: string[]) => {
request.get<PendingApprovalTask[]>(`${DEPLOY_URL}/my-approval-tasks`); const params = workflowDefinitionKeys && workflowDefinitionKeys.length > 0
? { workflowDefinitionKeys: workflowDefinitionKeys.join(',') }
: {};
return request.get<PendingApprovalTask[]>(`${DEPLOY_URL}/my-approval-tasks`, { params });
};
/** /**
* *

View File

@ -9,7 +9,7 @@ import {
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; 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 type { WorkflowDefinition } from '../types';
import { getDefinitionById as getFormDefinitionById } from '@/pages/Form/Definition/List/service'; import { getDefinitionById as getFormDefinitionById } from '@/pages/Form/Definition/List/service';
import type { FormDefinitionResponse } from '@/pages/Form/Definition/List/types'; import type { FormDefinitionResponse } from '@/pages/Form/Definition/List/types';
@ -56,7 +56,7 @@ const DeployDialog: React.FC<DeployDialogProps> = ({
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[500px]">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-green-600"> <DialogTitle className="flex items-center gap-2 text-green-600">
<CheckCircle2 className="h-5 w-5" /> <Rocket className="h-5 w-5" />
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
"<span className="font-semibold text-foreground">{record.name}</span>" "<span className="font-semibold text-foreground">{record.name}</span>"
@ -104,7 +104,7 @@ const DeployDialog: React.FC<DeployDialogProps> = ({
</Button> </Button>
<Button onClick={onConfirm}> <Button onClick={onConfirm}>
<CheckCircle2 className="h-4 w-4 mr-2" /> <Rocket className="h-4 w-4 mr-2" />
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DataTablePagination } from '@/components/ui/pagination'; import { DataTablePagination } from '@/components/ui/pagination';
import { import {
Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2, Loader2, Plus, Search, Edit, Trash2, Play, CheckCircle2, Rocket,
Clock, Activity, Workflow, Eye, Pencil, FolderKanban Clock, Activity, Workflow, Eye, Pencil, FolderKanban
} from 'lucide-react'; } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
@ -486,7 +486,7 @@ const WorkflowDefinitionList: React.FC = () => {
title="发布" title="发布"
className="text-green-600 hover:text-green-700 hover:bg-green-50" className="text-green-600 hover:text-green-700 hover:bg-green-50"
> >
<CheckCircle2 className="h-4 w-4" /> <Rocket className="h-4 w-4" />
</Button> </Button>
</> </>
) : ( ) : (

View File

@ -371,6 +371,12 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
return Array.isArray(currentValue) && currentValue.includes(expectedValue); return Array.isArray(currentValue) && currentValue.includes(expectedValue);
case 'notIncludes': case 'notIncludes':
return Array.isArray(currentValue) && !currentValue.includes(expectedValue); 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: default:
return currentValue === expectedValue; return currentValue === expectedValue;
} }