重构前端逻辑
This commit is contained in:
parent
9acae67438
commit
03814728f1
@ -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<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 isNotStarted = status === 'NOT_STARTED';
|
||||
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',
|
||||
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<DeployFlowGraphModalProps> = ({
|
||||
return map;
|
||||
}, [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(() => {
|
||||
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<DeployFlowGraphModalProps> = ({
|
||||
return timeA - timeB;
|
||||
});
|
||||
|
||||
// 创建节点位置映射(已执行的节点)
|
||||
// 线性布局:从左到右排列
|
||||
const nodePositionMap = new Map<string, { x: number; y: number }>();
|
||||
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<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) => {
|
||||
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,29 +497,67 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
||||
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 isRunning = flowData.runningNodeCount > 0;
|
||||
const displayedNodeIds = new Set(flowNodes.map(n => n.id));
|
||||
|
||||
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'; // 默认灰色
|
||||
if (sourceStatus === 'COMPLETED') {
|
||||
let strokeWidth = 2;
|
||||
let animated = false;
|
||||
let opacity = 1;
|
||||
|
||||
// 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线
|
||||
if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) {
|
||||
strokeColor = '#10b981'; // 绿色
|
||||
} else if (sourceStatus === 'FAILED') {
|
||||
}
|
||||
// 源节点失败 = 红色
|
||||
else if (sourceStatus === 'FAILED') {
|
||||
strokeColor = '#ef4444'; // 红色
|
||||
} else if (sourceStatus === 'RUNNING') {
|
||||
}
|
||||
// 源节点运行中 = 蓝色动画
|
||||
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 {
|
||||
@ -481,11 +565,12 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
||||
source,
|
||||
target,
|
||||
type: 'smoothstep',
|
||||
// 不显示边的标签(条件表达式等)
|
||||
animated: sourceStatus === 'RUNNING',
|
||||
animated,
|
||||
style: {
|
||||
stroke: strokeColor,
|
||||
strokeWidth: 2,
|
||||
strokeWidth,
|
||||
opacity,
|
||||
strokeDasharray: (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED' && isReachableEdge) ? '5,5' : undefined,
|
||||
},
|
||||
markerEnd: {
|
||||
type: 'arrowclosed',
|
||||
@ -495,7 +580,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [flowData, nodeInstanceMap]);
|
||||
}, [flowData, nodeInstanceMap, flowNodes, reachableNodes]);
|
||||
|
||||
// 获取部署状态信息
|
||||
const deployStatusInfo = flowData
|
||||
@ -559,7 +644,11 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
||||
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}
|
||||
>
|
||||
<Background
|
||||
variant={BackgroundVariant.Dots}
|
||||
@ -567,21 +656,6 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
||||
size={1}
|
||||
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>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
|
||||
@ -38,11 +38,13 @@ import { DeployFlowGraphModal } from './DeployFlowGraphModal';
|
||||
interface PendingApprovalModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workflowDefinitionKeys?: string[]; // 工作流定义键列表
|
||||
}
|
||||
|
||||
export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
workflowDefinitionKeys,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -59,7 +61,7 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
|
||||
const loadApprovalList = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getMyApprovalTasks();
|
||||
const response = await getMyApprovalTasks(workflowDefinitionKeys);
|
||||
if (response) {
|
||||
setApprovalList(response || []);
|
||||
}
|
||||
|
||||
@ -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 && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{currentTeam.teamName}</span>
|
||||
{currentTeam.teamRole && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{currentTeam.teamRole}</span>
|
||||
</>
|
||||
)}
|
||||
{currentTeam.description && (
|
||||
<>
|
||||
<span>·</span>
|
||||
@ -453,6 +459,7 @@ const Dashboard: React.FC = () => {
|
||||
<PendingApprovalModal
|
||||
open={approvalModalOpen}
|
||||
onOpenChange={setApprovalModalOpen}
|
||||
workflowDefinitionKeys={workflowDefinitionKeys}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -25,9 +25,14 @@ export const getDeployRecordFlowGraph = (deployRecordId: number) =>
|
||||
|
||||
/**
|
||||
* 获取我的待审批任务列表
|
||||
* @param workflowDefinitionKeys 工作流定义键列表(可选)
|
||||
*/
|
||||
export const getMyApprovalTasks = () =>
|
||||
request.get<PendingApprovalTask[]>(`${DEPLOY_URL}/my-approval-tasks`);
|
||||
export const getMyApprovalTasks = (workflowDefinitionKeys?: string[]) => {
|
||||
const params = workflowDefinitionKeys && workflowDefinitionKeys.length > 0
|
||||
? { workflowDefinitionKeys: workflowDefinitionKeys.join(',') }
|
||||
: {};
|
||||
return request.get<PendingApprovalTask[]>(`${DEPLOY_URL}/my-approval-tasks`, { params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 完成审批
|
||||
|
||||
@ -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<DeployDialogProps> = ({
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-green-600">
|
||||
<CheckCircle2 className="h-5 w-5" /> 确认发布工作流?
|
||||
<Rocket className="h-5 w-5" /> 确认发布工作流?
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
您确定要发布工作流 "<span className="font-semibold text-foreground">{record.name}</span>" 吗?
|
||||
@ -104,7 +104,7 @@ const DeployDialog: React.FC<DeployDialogProps> = ({
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
||||
<Rocket className="h-4 w-4 mr-2" />
|
||||
确认发布
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@ -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"
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
<Rocket className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@ -371,6 +371,12 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user