表单CRUD
This commit is contained in:
parent
26663ef9df
commit
2868e294d3
@ -77,6 +77,8 @@ export const useWorkflowSave = () => {
|
|||||||
from: edge.source, // 后端使用from字段
|
from: edge.source, // 后端使用from字段
|
||||||
to: edge.target, // 后端使用to字段
|
to: edge.target, // 后端使用to字段
|
||||||
name: edge.data?.label || "", // 边的名称
|
name: edge.data?.label || "", // 边的名称
|
||||||
|
sourceHandle: edge.sourceHandle, // 保存源连接点
|
||||||
|
targetHandle: edge.targetHandle, // 保存目标连接点
|
||||||
config: {
|
config: {
|
||||||
type: "sequence", // 固定为sequence类型
|
type: "sequence", // 固定为sequence类型
|
||||||
condition: edge.data?.condition // 保存边条件
|
condition: edge.data?.condition // 保存边条件
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { Dialog, DialogPortal, DialogOverlay, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogPortal, DialogOverlay, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { WorkflowHistoricalInstance, WorkflowInstanceStage } from '../types';
|
|
||||||
import ReactFlow, { Background, Controls, MiniMap, Node, Edge } from 'reactflow';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import 'reactflow/dist/style.css';
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||||
import { X, Clock, CheckCircle2, XCircle, AlertCircle } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { WorkflowHistoricalInstance } from '../types';
|
||||||
|
import { NodeType } from '../../Design/nodes/types';
|
||||||
|
import FlowGraphTab from './tabs/FlowGraphTab';
|
||||||
|
import TimelineTab from './tabs/TimelineTab';
|
||||||
|
import DetailInfoTab from './tabs/DetailInfoTab';
|
||||||
|
|
||||||
interface DetailModalProps {
|
interface DetailModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -20,340 +20,135 @@ interface DetailModalProps {
|
|||||||
const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceData }) => {
|
const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceData }) => {
|
||||||
if (!instanceData) return null;
|
if (!instanceData) return null;
|
||||||
|
|
||||||
|
// 获取状态徽章
|
||||||
const getStatusBadge = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
const statusMap: Record<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success'; text: string }> = {
|
const statusMap: Record<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success'; text: string; className?: string }> = {
|
||||||
COMPLETED: { variant: 'success', text: '已完成' },
|
COMPLETED: {
|
||||||
RUNNING: { variant: 'default', text: '运行中' },
|
variant: 'success',
|
||||||
FAILED: { variant: 'destructive', text: '失败' },
|
text: '已完成',
|
||||||
TERMINATED: { variant: 'secondary', text: '已终止' },
|
className: 'bg-green-100 text-green-700 border-green-200'
|
||||||
NOT_STARTED: { variant: 'outline', text: '未执行' }
|
|
||||||
};
|
|
||||||
const statusInfo = statusMap[status] || { variant: 'outline', text: status };
|
|
||||||
return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNodeTypeText = (nodeType: string) => {
|
|
||||||
const nodeTypeMap: Record<string, string> = {
|
|
||||||
START_EVENT: '开始节点',
|
|
||||||
END_EVENT: '结束节点',
|
|
||||||
SERVICE_TASK: '服务任务',
|
|
||||||
USER_TASK: '用户任务',
|
|
||||||
SCRIPT_TASK: '脚本任务',
|
|
||||||
NOTIFICATION: '通知任务',
|
|
||||||
JENKINS_BUILD: 'Jenkins构建',
|
|
||||||
APPROVAL: '审批任务',
|
|
||||||
StartEvent: '开始节点',
|
|
||||||
EndEvent: '结束节点',
|
|
||||||
ServiceTask: '服务任务',
|
|
||||||
UserTask: '用户任务',
|
|
||||||
ScriptTask: '脚本任务'
|
|
||||||
};
|
|
||||||
return nodeTypeMap[nodeType] || nodeType;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusIcon = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'COMPLETED':
|
|
||||||
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
|
||||||
case 'RUNNING':
|
|
||||||
return <Clock className="h-4 w-4 text-blue-500 animate-pulse" />;
|
|
||||||
case 'FAILED':
|
|
||||||
return <XCircle className="h-4 w-4 text-red-500" />;
|
|
||||||
default:
|
|
||||||
return <AlertCircle className="h-4 w-4 text-gray-400" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 构建节点状态映射
|
|
||||||
const nodeStatusMap = useMemo(() => {
|
|
||||||
const map = new Map<string, WorkflowInstanceStage>();
|
|
||||||
instanceData.stages.forEach(stage => {
|
|
||||||
map.set(stage.nodeId, stage);
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
}, [instanceData.stages]);
|
|
||||||
|
|
||||||
// 获取节点状态
|
|
||||||
const getNodeStatus = (nodeId: string): string => {
|
|
||||||
const stage = nodeStatusMap.get(nodeId);
|
|
||||||
return stage?.status || 'NOT_STARTED';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取节点颜色
|
|
||||||
const getNodeColor = (status: string) => {
|
|
||||||
const colorMap: Record<string, string> = {
|
|
||||||
COMPLETED: '#52c41a',
|
|
||||||
RUNNING: '#1890ff',
|
|
||||||
FAILED: '#ff4d4f',
|
|
||||||
TERMINATED: '#faad14',
|
|
||||||
NOT_STARTED: '#d9d9d9'
|
|
||||||
};
|
|
||||||
return colorMap[status] || '#d9d9d9';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 转换为React Flow节点
|
|
||||||
const flowNodes: Node[] = useMemo(() => {
|
|
||||||
if (!instanceData.graph?.nodes) return [];
|
|
||||||
|
|
||||||
return instanceData.graph.nodes.map(node => {
|
|
||||||
const status = getNodeStatus(node.id);
|
|
||||||
const stage = nodeStatusMap.get(node.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: node.id,
|
|
||||||
type: 'default',
|
|
||||||
position: { x: node.position.x + 400, y: node.position.y + 200 },
|
|
||||||
data: {
|
|
||||||
label: (
|
|
||||||
<div style={{ textAlign: 'center', minWidth: '80px' }}>
|
|
||||||
<div style={{
|
|
||||||
fontSize: '12px',
|
|
||||||
fontWeight: 500,
|
|
||||||
marginBottom: '4px'
|
|
||||||
}}>
|
|
||||||
{node.nodeName}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '10px', color: '#666' }}>
|
|
||||||
{getNodeTypeText(node.nodeType)}
|
|
||||||
</div>
|
|
||||||
{stage && (
|
|
||||||
<div style={{ fontSize: '10px', marginTop: '4px' }}>
|
|
||||||
{getStatusBadge(status)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
style: {
|
RUNNING: {
|
||||||
background: getNodeColor(status),
|
variant: 'default',
|
||||||
color: status === 'NOT_STARTED' ? '#333' : '#fff',
|
text: '运行中',
|
||||||
border: `2px solid ${getNodeColor(status)}`,
|
className: 'bg-blue-100 text-blue-700 border-blue-200 animate-pulse'
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '10px',
|
|
||||||
minWidth: '120px',
|
|
||||||
boxShadow: status === 'RUNNING' ? '0 0 10px rgba(24, 144, 255, 0.5)' : undefined
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [instanceData.graph, nodeStatusMap]);
|
|
||||||
|
|
||||||
// 判断连线是否已执行
|
|
||||||
const isEdgeExecuted = (fromNodeId: string, _toNodeId: string): boolean => {
|
|
||||||
const fromStage = nodeStatusMap.get(fromNodeId);
|
|
||||||
|
|
||||||
// 如果起始节点已完成,则连线已执行
|
|
||||||
return fromStage?.status === 'COMPLETED' || fromStage?.status === 'RUNNING';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 转换为React Flow边
|
|
||||||
const flowEdges: Edge[] = useMemo(() => {
|
|
||||||
if (!instanceData.graph?.edges) return [];
|
|
||||||
|
|
||||||
return instanceData.graph.edges.map(edge => {
|
|
||||||
const executed = isEdgeExecuted(edge.from, edge.to);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: edge.id,
|
|
||||||
source: edge.from,
|
|
||||||
target: edge.to,
|
|
||||||
label: edge.name || undefined,
|
|
||||||
type: 'smoothstep',
|
|
||||||
animated: isEdgeExecuted(edge.from, edge.to) && getNodeStatus(edge.to) === 'RUNNING',
|
|
||||||
style: {
|
|
||||||
stroke: executed ? '#52c41a' : '#d9d9d9',
|
|
||||||
strokeWidth: 2
|
|
||||||
},
|
},
|
||||||
labelStyle: {
|
FAILED: {
|
||||||
fontSize: 10,
|
variant: 'destructive',
|
||||||
fill: '#666'
|
text: '失败',
|
||||||
|
className: 'bg-red-100 text-red-700 border-red-200'
|
||||||
|
},
|
||||||
|
TERMINATED: {
|
||||||
|
variant: 'secondary',
|
||||||
|
text: '已终止',
|
||||||
|
className: 'bg-orange-100 text-orange-700 border-orange-200'
|
||||||
|
},
|
||||||
|
NOT_STARTED: {
|
||||||
|
variant: 'outline',
|
||||||
|
text: '未执行',
|
||||||
|
className: 'bg-gray-50 text-gray-600 border-gray-300'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
const statusInfo = statusMap[status] || statusMap.NOT_STARTED;
|
||||||
}, [instanceData.graph, nodeStatusMap]);
|
return (
|
||||||
|
<Badge
|
||||||
// 描述信息组件
|
variant={statusInfo.variant}
|
||||||
const DescriptionItem = ({ label, value }: { label: string; value: React.ReactNode }) => (
|
className={cn('font-medium', statusInfo.className)}
|
||||||
<div className="flex items-start py-2 border-b last:border-b-0">
|
>
|
||||||
<div className="w-32 text-sm font-medium text-muted-foreground flex-shrink-0">{label}</div>
|
{statusInfo.text}
|
||||||
<div className="flex-1 text-sm">{value}</div>
|
</Badge>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取节点类型文本
|
||||||
|
const getNodeTypeText = (nodeType: string) => {
|
||||||
|
const nodeTypeMap: Record<NodeType, string> = {
|
||||||
|
[NodeType.START_EVENT]: '开始节点',
|
||||||
|
[NodeType.END_EVENT]: '结束节点',
|
||||||
|
[NodeType.SERVICE_TASK]: '服务任务',
|
||||||
|
[NodeType.USER_TASK]: '用户任务',
|
||||||
|
[NodeType.SCRIPT_TASK]: '脚本任务',
|
||||||
|
[NodeType.NOTIFICATION]: '通知任务',
|
||||||
|
[NodeType.JENKINS_BUILD]: 'Jenkins构建',
|
||||||
|
[NodeType.APPROVAL]: '审批任务',
|
||||||
|
[NodeType.DEPLOY_NODE]: '部署节点',
|
||||||
|
[NodeType.GATEWAY_NODE]: '网关节点',
|
||||||
|
[NodeType.SUB_PROCESS]: '子流程',
|
||||||
|
[NodeType.CALL_ACTIVITY]: '调用活动',
|
||||||
|
};
|
||||||
|
return nodeTypeMap[nodeType as NodeType] || nodeType;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||||
<DialogPortal>
|
<DialogPortal>
|
||||||
<DialogOverlay className="z-[60]" />
|
<DialogOverlay className="z-[60] backdrop-blur-sm" />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed left-[50%] top-[50%] z-[60] grid w-full max-w-7xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[90vh] overflow-y-auto"
|
"fixed left-[50%] top-[50%] z-[60] grid w-full max-w-[90vw]",
|
||||||
|
"translate-x-[-50%] translate-y-[-50%]",
|
||||||
|
"gap-4 border-2 bg-background p-6 shadow-2xl",
|
||||||
|
"duration-300",
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||||
|
"data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
|
||||||
|
"data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
||||||
|
"sm:rounded-xl",
|
||||||
|
"max-h-[90vh] overflow-y-auto"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close
|
||||||
<X className="h-4 w-4" />
|
className="absolute right-6 top-6 rounded-md opacity-70 ring-offset-background transition-all hover:opacity-100 hover:bg-accent hover:scale-110 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground p-1.5"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>流程执行详情</DialogTitle>
|
<DialogTitle className="text-2xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||||
|
流程执行详情
|
||||||
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Tabs defaultValue="graph" className="w-full">
|
<Tabs defaultValue="graph" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="grid w-full grid-cols-3 h-12">
|
||||||
<TabsTrigger value="graph">流程图</TabsTrigger>
|
<TabsTrigger value="graph" className="text-base font-medium">
|
||||||
<TabsTrigger value="timeline">执行时间线</TabsTrigger>
|
📊 流程图
|
||||||
<TabsTrigger value="info">详细信息</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="timeline" className="text-base font-medium">
|
||||||
|
⏱️ 执行时间线
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="info" className="text-base font-medium">
|
||||||
|
📋 详细信息
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* 流程图标签页 */}
|
{/* 流程图标签页 */}
|
||||||
<TabsContent value="graph" className="space-y-4">
|
<TabsContent value="graph" className="mt-6">
|
||||||
<Card>
|
<FlowGraphTab
|
||||||
<CardContent className="pt-6">
|
instanceData={instanceData}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
getStatusBadge={getStatusBadge}
|
||||||
<DescriptionItem label="业务标识" value={instanceData.businessKey} />
|
getNodeTypeText={getNodeTypeText}
|
||||||
<DescriptionItem label="状态" value={getStatusBadge(instanceData.status)} />
|
|
||||||
<DescriptionItem
|
|
||||||
label="开始时间"
|
|
||||||
value={dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
/>
|
/>
|
||||||
<DescriptionItem
|
|
||||||
label="结束时间"
|
|
||||||
value={instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{instanceData.graph ? (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>流程执行图</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div style={{ height: '400px', border: '1px solid hsl(var(--border))', borderRadius: '8px' }}>
|
|
||||||
<ReactFlow
|
|
||||||
nodes={flowNodes}
|
|
||||||
edges={flowEdges}
|
|
||||||
fitView
|
|
||||||
minZoom={0.5}
|
|
||||||
maxZoom={1.5}
|
|
||||||
defaultViewport={{ x: 0, y: 0, zoom: 0.8 }}
|
|
||||||
>
|
|
||||||
<Background />
|
|
||||||
<Controls />
|
|
||||||
<MiniMap
|
|
||||||
nodeColor={(node: Node) => {
|
|
||||||
const status = getNodeStatus(node.id);
|
|
||||||
return getNodeColor(status);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ReactFlow>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex gap-4 justify-center text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 bg-green-500 rounded" />
|
|
||||||
<span>已完成</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 bg-blue-500 rounded" />
|
|
||||||
<span>运行中</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 bg-red-500 rounded" />
|
|
||||||
<span>失败</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 bg-gray-300 rounded" />
|
|
||||||
<span>未执行</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<div className="text-center py-10 text-muted-foreground">
|
|
||||||
暂无流程图数据
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 执行时间线标签页 */}
|
{/* 执行时间线标签页 */}
|
||||||
<TabsContent value="timeline">
|
<TabsContent value="timeline" className="mt-6">
|
||||||
<Card>
|
<TimelineTab
|
||||||
<CardContent className="pt-6">
|
instanceData={instanceData}
|
||||||
<div className="relative space-y-4">
|
getStatusBadge={getStatusBadge}
|
||||||
{instanceData.stages.map((stage, index) => (
|
getNodeTypeText={getNodeTypeText}
|
||||||
<div key={stage.id || index} className="flex gap-4">
|
/>
|
||||||
{/* 时间线图标 */}
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<div className="flex items-center justify-center w-8 h-8 rounded-full border-2 bg-background z-10"
|
|
||||||
style={{
|
|
||||||
borderColor: stage.status === 'COMPLETED' ? '#52c41a' :
|
|
||||||
stage.status === 'FAILED' ? '#ff4d4f' :
|
|
||||||
stage.status === 'RUNNING' ? '#1890ff' : '#d9d9d9'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getStatusIcon(stage.status)}
|
|
||||||
</div>
|
|
||||||
{index < instanceData.stages.length - 1 && (
|
|
||||||
<div className="w-0.5 h-full bg-border mt-1" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 时间线内容 */}
|
|
||||||
<div className="flex-1 pb-8">
|
|
||||||
<div className="font-medium mb-1 flex items-center gap-2">
|
|
||||||
{stage.nodeName}
|
|
||||||
<span className="text-xs text-muted-foreground font-normal">
|
|
||||||
{getNodeTypeText(stage.nodeType)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground mb-2">
|
|
||||||
{stage.startTime && dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
{stage.endTime && ` → ${dayjs(stage.endTime).format('HH:mm:ss')}`}
|
|
||||||
{stage.startTime && stage.endTime && (
|
|
||||||
<span className="ml-2">
|
|
||||||
(耗时: {dayjs(stage.endTime).diff(dayjs(stage.startTime), 'second')}秒)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>{getStatusBadge(stage.status)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 详细信息标签页 */}
|
{/* 详细信息标签页 */}
|
||||||
<TabsContent value="info">
|
<TabsContent value="info" className="mt-6">
|
||||||
<Card>
|
<DetailInfoTab
|
||||||
<CardContent className="pt-6">
|
instanceData={instanceData}
|
||||||
<div className="space-y-0">
|
getStatusBadge={getStatusBadge}
|
||||||
<DescriptionItem label="流程实例ID" value={instanceData.processInstanceId} />
|
|
||||||
<DescriptionItem label="流程定义ID" value={instanceData.processDefinitionId} />
|
|
||||||
<DescriptionItem label="业务标识" value={instanceData.businessKey} />
|
|
||||||
<DescriptionItem label="状态" value={getStatusBadge(instanceData.status)} />
|
|
||||||
<DescriptionItem
|
|
||||||
label="开始时间"
|
|
||||||
value={dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
/>
|
/>
|
||||||
<DescriptionItem
|
|
||||||
label="结束时间"
|
|
||||||
value={instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
|
|
||||||
/>
|
|
||||||
{instanceData.startTime && instanceData.endTime && (
|
|
||||||
<DescriptionItem
|
|
||||||
label="总耗时"
|
|
||||||
value={`${dayjs(instanceData.endTime).diff(dayjs(instanceData.startTime), 'second')}秒`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<DescriptionItem label="执行节点数" value={instanceData.stages.length} />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
|
|||||||
@ -0,0 +1,219 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
FileText, Hash, Key, Calendar, Clock, Activity,
|
||||||
|
CheckCircle, List, Timer
|
||||||
|
} from 'lucide-react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { WorkflowHistoricalInstance } from '../../types';
|
||||||
|
|
||||||
|
interface DetailInfoTabProps {
|
||||||
|
instanceData: WorkflowHistoricalInstance;
|
||||||
|
getStatusBadge: (status: string) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DetailInfoTab: React.FC<DetailInfoTabProps> = ({
|
||||||
|
instanceData,
|
||||||
|
getStatusBadge
|
||||||
|
}) => {
|
||||||
|
// 信息项组件
|
||||||
|
const InfoItem = ({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
valueClass = ''
|
||||||
|
}: {
|
||||||
|
icon: any;
|
||||||
|
label: string;
|
||||||
|
value: React.ReactNode;
|
||||||
|
valueClass?: string;
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-start py-3 px-4 rounded-lg hover:bg-accent/50 transition-colors">
|
||||||
|
<div className="flex items-center gap-3 w-48 flex-shrink-0">
|
||||||
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">{label}</span>
|
||||||
|
</div>
|
||||||
|
<div className={`flex-1 text-sm ${valueClass}`}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算总耗时
|
||||||
|
const totalDuration = instanceData.startTime && instanceData.endTime
|
||||||
|
? dayjs(instanceData.endTime).diff(dayjs(instanceData.startTime), 'second')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// 格式化耗时
|
||||||
|
const formatDuration = (seconds: number) => {
|
||||||
|
if (seconds < 60) return `${seconds}秒`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
if (minutes < 60) return `${minutes}分${remainingSeconds}秒`;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainingMinutes = minutes % 60;
|
||||||
|
return `${hours}小时${remainingMinutes}分${remainingSeconds}秒`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4 pb-2 border-b">
|
||||||
|
<FileText className="h-5 w-5 text-primary" />
|
||||||
|
<h3 className="font-semibold text-lg">基本信息</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<InfoItem
|
||||||
|
icon={Hash}
|
||||||
|
label="流程实例ID"
|
||||||
|
value={
|
||||||
|
<span className="font-mono text-xs break-all">
|
||||||
|
{instanceData.processInstanceId}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InfoItem
|
||||||
|
icon={Hash}
|
||||||
|
label="流程定义ID"
|
||||||
|
value={
|
||||||
|
<span className="font-mono text-xs break-all">
|
||||||
|
{instanceData.processDefinitionId}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InfoItem
|
||||||
|
icon={Key}
|
||||||
|
label="业务标识"
|
||||||
|
value={
|
||||||
|
<span className="font-mono font-semibold">
|
||||||
|
{instanceData.businessKey}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InfoItem
|
||||||
|
icon={CheckCircle}
|
||||||
|
label="执行状态"
|
||||||
|
value={getStatusBadge(instanceData.status)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 时间信息 */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4 pb-2 border-b">
|
||||||
|
<Clock className="h-5 w-5 text-primary" />
|
||||||
|
<h3 className="font-semibold text-lg">时间信息</h3>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<InfoItem
|
||||||
|
icon={Calendar}
|
||||||
|
label="开始时间"
|
||||||
|
value={
|
||||||
|
<span className="font-mono">
|
||||||
|
{dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InfoItem
|
||||||
|
icon={Calendar}
|
||||||
|
label="结束时间"
|
||||||
|
value={
|
||||||
|
instanceData.endTime ? (
|
||||||
|
<span className="font-mono">
|
||||||
|
{dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-blue-500 font-medium flex items-center gap-2">
|
||||||
|
<Activity className="h-4 w-4 animate-pulse" />
|
||||||
|
进行中...
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{totalDuration !== null && (
|
||||||
|
<InfoItem
|
||||||
|
icon={Timer}
|
||||||
|
label="总耗时"
|
||||||
|
value={
|
||||||
|
<span className="font-semibold text-primary text-base">
|
||||||
|
{formatDuration(totalDuration)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 执行统计 */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4 pb-2 border-b">
|
||||||
|
<List className="h-5 w-5 text-primary" />
|
||||||
|
<h3 className="font-semibold text-lg">执行统计</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* 总节点数 */}
|
||||||
|
<div className="p-4 rounded-lg bg-blue-50 border border-blue-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-blue-600 mb-1">执行节点数</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-700">
|
||||||
|
{instanceData.stages.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<List className="h-8 w-8 text-blue-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 已完成节点 */}
|
||||||
|
<div className="p-4 rounded-lg bg-green-50 border border-green-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-green-600 mb-1">已完成节点</p>
|
||||||
|
<p className="text-2xl font-bold text-green-700">
|
||||||
|
{instanceData.stages.filter(s => s.status === 'COMPLETED').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CheckCircle className="h-8 w-8 text-green-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 运行中节点 */}
|
||||||
|
<div className="p-4 rounded-lg bg-orange-50 border border-orange-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-orange-600 mb-1">运行中节点</p>
|
||||||
|
<p className="text-2xl font-bold text-orange-700">
|
||||||
|
{instanceData.stages.filter(s => s.status === 'RUNNING').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Activity className="h-8 w-8 text-orange-300 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 失败节点 */}
|
||||||
|
<div className="p-4 rounded-lg bg-red-50 border border-red-200">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-red-600 mb-1">失败节点</p>
|
||||||
|
<p className="text-2xl font-bold text-red-700">
|
||||||
|
{instanceData.stages.filter(s => s.status === 'FAILED').length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<activity className="h-8 w-8 text-red-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailInfoTab;
|
||||||
|
|
||||||
@ -0,0 +1,334 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { ReactFlow, Background, Controls, MiniMap, Node, Edge, Handle, Position, BackgroundVariant } from '@xyflow/react';
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import { WorkflowHistoricalInstance, WorkflowInstanceStage } from '../../types';
|
||||||
|
import { NodeType } from '../../../Design/nodes/types';
|
||||||
|
|
||||||
|
interface FlowGraphTabProps {
|
||||||
|
instanceData: WorkflowHistoricalInstance;
|
||||||
|
getStatusBadge: (status: string) => React.ReactNode;
|
||||||
|
getNodeTypeText: (nodeType: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义节点组件 - 增强版
|
||||||
|
const EnhancedCustomNode: React.FC<any> = ({ data, selected }) => {
|
||||||
|
const nodeType = data.nodeType as NodeType;
|
||||||
|
const status = data.status;
|
||||||
|
|
||||||
|
// 根据状态判断是否需要虚线边框
|
||||||
|
const isNotStarted = status === 'NOT_STARTED';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative group">
|
||||||
|
{/* 输入连接点 */}
|
||||||
|
{nodeType !== NodeType.START_EVENT && (
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
className="!w-3 !h-3 !rounded-full !border-2 !border-white !shadow-md transition-transform hover:!scale-125"
|
||||||
|
style={{ background: data.handleColor || '#1890ff' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 节点内容 */}
|
||||||
|
<Card
|
||||||
|
className={`
|
||||||
|
min-w-[140px] max-w-[240px] transition-all duration-200
|
||||||
|
${selected ? 'ring-2 ring-primary ring-offset-2' : ''}
|
||||||
|
${isNotStarted ? 'border-dashed border-2' : 'shadow-md'}
|
||||||
|
hover:shadow-xl hover:scale-105
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
background: data.background,
|
||||||
|
color: data.textColor,
|
||||||
|
borderColor: data.borderColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
{data.label}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 输出连接点 */}
|
||||||
|
{nodeType !== NodeType.END_EVENT && (
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
className="!w-3 !h-3 !rounded-full !border-2 !border-white !shadow-md transition-transform hover:!scale-125"
|
||||||
|
style={{ background: data.handleColor || '#1890ff' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
enhanced: EnhancedCustomNode
|
||||||
|
};
|
||||||
|
|
||||||
|
const FlowGraphTab: React.FC<FlowGraphTabProps> = ({
|
||||||
|
instanceData,
|
||||||
|
getStatusBadge,
|
||||||
|
getNodeTypeText
|
||||||
|
}) => {
|
||||||
|
// 构建节点状态映射
|
||||||
|
const nodeStatusMap = useMemo(() => {
|
||||||
|
const map = new Map<string, WorkflowInstanceStage>();
|
||||||
|
instanceData.stages.forEach(stage => {
|
||||||
|
map.set(stage.nodeId, stage);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [instanceData.stages]);
|
||||||
|
|
||||||
|
// 获取节点状态
|
||||||
|
const getNodeStatus = (nodeId: string): string => {
|
||||||
|
const stage = nodeStatusMap.get(nodeId);
|
||||||
|
return stage?.status || 'NOT_STARTED';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取节点颜色
|
||||||
|
const getNodeColor = (status: string) => {
|
||||||
|
const colorMap: Record<string, { bg: string; border: string; text: string }> = {
|
||||||
|
COMPLETED: {
|
||||||
|
bg: 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)',
|
||||||
|
border: '#52c41a',
|
||||||
|
text: '#fff'
|
||||||
|
},
|
||||||
|
RUNNING: {
|
||||||
|
bg: 'linear-gradient(135deg, #1890ff 0%, #40a9ff 100%)',
|
||||||
|
border: '#1890ff',
|
||||||
|
text: '#fff'
|
||||||
|
},
|
||||||
|
FAILED: {
|
||||||
|
bg: 'linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)',
|
||||||
|
border: '#ff4d4f',
|
||||||
|
text: '#fff'
|
||||||
|
},
|
||||||
|
TERMINATED: {
|
||||||
|
bg: 'linear-gradient(135deg, #faad14 0%, #ffc53d 100%)',
|
||||||
|
border: '#faad14',
|
||||||
|
text: '#fff'
|
||||||
|
},
|
||||||
|
NOT_STARTED: {
|
||||||
|
bg: '#fafafa',
|
||||||
|
border: '#d9d9d9',
|
||||||
|
text: '#666'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return colorMap[status] || colorMap.NOT_STARTED;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 转换为React Flow节点
|
||||||
|
const flowNodes: Node[] = useMemo(() => {
|
||||||
|
if (!instanceData.graph?.nodes) return [];
|
||||||
|
|
||||||
|
return instanceData.graph.nodes.map(node => {
|
||||||
|
const status = getNodeStatus(node.id);
|
||||||
|
const stage = nodeStatusMap.get(node.id);
|
||||||
|
const colors = getNodeColor(status);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
type: 'enhanced',
|
||||||
|
position: { x: node.position.x, y: node.position.y },
|
||||||
|
data: {
|
||||||
|
nodeType: node.nodeType,
|
||||||
|
status: status,
|
||||||
|
handleColor: colors.border,
|
||||||
|
background: colors.bg,
|
||||||
|
borderColor: colors.border,
|
||||||
|
textColor: colors.text,
|
||||||
|
label: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-semibold text-sm">
|
||||||
|
{node.nodeName}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs opacity-80">
|
||||||
|
{getNodeTypeText(node.nodeType)}
|
||||||
|
</div>
|
||||||
|
{stage && (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
{getStatusBadge(status)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{stage?.startTime && (
|
||||||
|
<div className="text-xs opacity-70 mt-1">
|
||||||
|
{dayjs(stage.startTime).format('HH:mm:ss')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [instanceData.graph, nodeStatusMap]);
|
||||||
|
|
||||||
|
// 判断连线是否已执行
|
||||||
|
const isEdgeExecuted = (fromNodeId: string): boolean => {
|
||||||
|
const fromStage = nodeStatusMap.get(fromNodeId);
|
||||||
|
return fromStage?.status === 'COMPLETED' || fromStage?.status === 'RUNNING';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 转换为React Flow边
|
||||||
|
const flowEdges: Edge[] = useMemo(() => {
|
||||||
|
if (!instanceData.graph?.edges) return [];
|
||||||
|
|
||||||
|
return instanceData.graph.edges.map(edge => {
|
||||||
|
const executed = isEdgeExecuted(edge.from);
|
||||||
|
const fromStatus = getNodeStatus(edge.from);
|
||||||
|
const toStatus = getNodeStatus(edge.to);
|
||||||
|
|
||||||
|
// 判断是否应该使用虚线
|
||||||
|
const isDashed = !executed || toStatus === 'NOT_STARTED';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: edge.id,
|
||||||
|
source: edge.from,
|
||||||
|
target: edge.to,
|
||||||
|
sourceHandle: edge.sourceHandle,
|
||||||
|
targetHandle: edge.targetHandle,
|
||||||
|
label: edge.name || undefined,
|
||||||
|
type: 'smoothstep',
|
||||||
|
animated: executed && toStatus === 'RUNNING',
|
||||||
|
style: {
|
||||||
|
stroke: executed ? '#52c41a' : '#d9d9d9',
|
||||||
|
strokeWidth: executed ? 3 : 2,
|
||||||
|
strokeDasharray: isDashed ? '5,5' : undefined, // 虚线效果
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: 'arrowclosed' as const,
|
||||||
|
color: executed ? '#52c41a' : '#d9d9d9',
|
||||||
|
},
|
||||||
|
labelStyle: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 500,
|
||||||
|
fill: executed ? '#52c41a' : '#999',
|
||||||
|
},
|
||||||
|
labelBgStyle: {
|
||||||
|
fill: '#fff',
|
||||||
|
fillOpacity: 0.9,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [instanceData.graph, nodeStatusMap]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 基本信息卡片 */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-start py-2">
|
||||||
|
<div className="w-24 text-sm font-medium text-muted-foreground flex-shrink-0">业务标识</div>
|
||||||
|
<div className="flex-1 text-sm font-mono">{instanceData.businessKey}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start py-2">
|
||||||
|
<div className="w-24 text-sm font-medium text-muted-foreground flex-shrink-0">状态</div>
|
||||||
|
<div className="flex-1">{getStatusBadge(instanceData.status)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start py-2">
|
||||||
|
<div className="w-24 text-sm font-medium text-muted-foreground flex-shrink-0">开始时间</div>
|
||||||
|
<div className="flex-1 text-sm">{dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start py-2">
|
||||||
|
<div className="w-24 text-sm font-medium text-muted-foreground flex-shrink-0">结束时间</div>
|
||||||
|
<div className="flex-1 text-sm">
|
||||||
|
{instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '进行中...'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 流程图卡片 */}
|
||||||
|
{instanceData.graph ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<span>流程执行图</span>
|
||||||
|
<span className="text-sm font-normal text-muted-foreground">
|
||||||
|
实时展示流程执行状态
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="rounded-lg border-2 border-border overflow-hidden" style={{ height: '500px' }}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={flowNodes}
|
||||||
|
edges={flowEdges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
minZoom={0.3}
|
||||||
|
maxZoom={2}
|
||||||
|
defaultViewport={{ x: 0, y: 0, zoom: 0.8 }}
|
||||||
|
attributionPosition="bottom-right"
|
||||||
|
>
|
||||||
|
<Background
|
||||||
|
variant={BackgroundVariant.Dots}
|
||||||
|
gap={16}
|
||||||
|
size={1}
|
||||||
|
color="#e0e0e0"
|
||||||
|
/>
|
||||||
|
<Controls
|
||||||
|
showInteractive={false}
|
||||||
|
className="!shadow-lg !border !border-border"
|
||||||
|
/>
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={(node: Node) => {
|
||||||
|
const status = getNodeStatus(node.id);
|
||||||
|
const colors = getNodeColor(status);
|
||||||
|
return colors.border;
|
||||||
|
}}
|
||||||
|
className="!shadow-lg !border !border-border"
|
||||||
|
maskColor="rgba(0, 0, 0, 0.05)"
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图例 */}
|
||||||
|
<div className="mt-4 flex flex-wrap gap-4 justify-center text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-gradient-to-br from-green-500 to-green-400 rounded shadow-sm" />
|
||||||
|
<span>已完成</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-gradient-to-br from-blue-500 to-blue-400 rounded shadow-sm animate-pulse" />
|
||||||
|
<span>运行中</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-gradient-to-br from-red-500 to-red-400 rounded shadow-sm" />
|
||||||
|
<span>失败</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-gray-200 border-2 border-dashed border-gray-400 rounded" />
|
||||||
|
<span>未执行</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-12 h-0.5 bg-green-500" />
|
||||||
|
<span>已执行路径</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-12 h-0.5 border-t-2 border-dashed border-gray-400" />
|
||||||
|
<span>未执行路径</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="text-center py-10 text-muted-foreground">
|
||||||
|
暂无流程图数据
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlowGraphTab;
|
||||||
|
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Clock, CheckCircle2, XCircle, AlertCircle, Play } from 'lucide-react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { WorkflowHistoricalInstance } from '../../types';
|
||||||
|
|
||||||
|
interface TimelineTabProps {
|
||||||
|
instanceData: WorkflowHistoricalInstance;
|
||||||
|
getStatusBadge: (status: string) => React.ReactNode;
|
||||||
|
getNodeTypeText: (nodeType: string) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimelineTab: React.FC<TimelineTabProps> = ({
|
||||||
|
instanceData,
|
||||||
|
getStatusBadge,
|
||||||
|
getNodeTypeText
|
||||||
|
}) => {
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'COMPLETED':
|
||||||
|
return <CheckCircle2 className="h-5 w-5 text-green-500" />;
|
||||||
|
case 'RUNNING':
|
||||||
|
return <Play className="h-5 w-5 text-blue-500 animate-pulse" />;
|
||||||
|
case 'FAILED':
|
||||||
|
return <XCircle className="h-5 w-5 text-red-500" />;
|
||||||
|
default:
|
||||||
|
return <AlertCircle className="h-5 w-5 text-gray-400" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'COMPLETED':
|
||||||
|
return {
|
||||||
|
border: 'border-green-500',
|
||||||
|
bg: 'bg-green-50',
|
||||||
|
line: 'bg-green-200'
|
||||||
|
};
|
||||||
|
case 'RUNNING':
|
||||||
|
return {
|
||||||
|
border: 'border-blue-500',
|
||||||
|
bg: 'bg-blue-50',
|
||||||
|
line: 'bg-blue-200'
|
||||||
|
};
|
||||||
|
case 'FAILED':
|
||||||
|
return {
|
||||||
|
border: 'border-red-500',
|
||||||
|
bg: 'bg-red-50',
|
||||||
|
line: 'bg-red-200'
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
border: 'border-gray-300',
|
||||||
|
bg: 'bg-gray-50',
|
||||||
|
line: 'bg-gray-200'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="relative space-y-6">
|
||||||
|
{instanceData.stages.map((stage, index) => {
|
||||||
|
const colors = getStatusColor(stage.status);
|
||||||
|
const duration = stage.startTime && stage.endTime
|
||||||
|
? dayjs(stage.endTime).diff(dayjs(stage.startTime), 'second')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={stage.id || index} className="flex gap-6 relative">
|
||||||
|
{/* 时间线左侧 */}
|
||||||
|
<div className="flex flex-col items-center w-16">
|
||||||
|
{/* 时间轴圆点 */}
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex items-center justify-center
|
||||||
|
w-10 h-10 rounded-full border-2 ${colors.border} ${colors.bg}
|
||||||
|
shadow-md transition-all duration-200
|
||||||
|
hover:scale-110 hover:shadow-lg
|
||||||
|
z-10
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{getStatusIcon(stage.status)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 连接线 */}
|
||||||
|
{index < instanceData.stages.length - 1 && (
|
||||||
|
<div className={`w-0.5 flex-1 ${colors.line} mt-2`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 时间线内容 */}
|
||||||
|
<div className="flex-1 pb-6">
|
||||||
|
<Card className="shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||||
|
<CardContent className="pt-4">
|
||||||
|
{/* 节点名称和类型 */}
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-base mb-1">
|
||||||
|
{stage.nodeName}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{getNodeTypeText(stage.nodeType)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{getStatusBadge(stage.status)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 时间信息 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{stage.startTime && (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">开始:</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stage.endTime && (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">结束:</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{dayjs(stage.endTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{duration !== null && (
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<span className="text-muted-foreground">耗时:</span>
|
||||||
|
<span className="font-semibold text-primary">
|
||||||
|
{duration < 60
|
||||||
|
? `${duration}秒`
|
||||||
|
: `${Math.floor(duration / 60)}分${duration % 60}秒`
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 空状态 */}
|
||||||
|
{instanceData.stages.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<AlertCircle className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
|
<p>暂无执行记录</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimelineTab;
|
||||||
|
|
||||||
@ -65,6 +65,8 @@ export interface WorkflowGraphEdge {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
vertices: any[];
|
vertices: any[];
|
||||||
|
sourceHandle?: string; // 源节点连接点
|
||||||
|
targetHandle?: string; // 目标节点连接点
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user