|
@@ -380,12 +380,69 @@ const WorkflowRenderer: React.FC<WorkflowRendererProps> = ({
|
|
|
// 获取 Timeline 的图标
|
|
// 获取 Timeline 的图标
|
|
|
const getTimelineDot = (status: 'completed' | 'in_progress' | 'pending') => {
|
|
const getTimelineDot = (status: 'completed' | 'in_progress' | 'pending') => {
|
|
|
if (status === 'completed') {
|
|
if (status === 'completed') {
|
|
|
- return <CheckCircleOutlined style={{ fontSize: '16px', color: '#52c41a' }} />;
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ width: '28px',
|
|
|
|
|
+ height: '28px',
|
|
|
|
|
+ minWidth: '28px',
|
|
|
|
|
+ minHeight: '28px',
|
|
|
|
|
+ maxWidth: '28px',
|
|
|
|
|
+ maxHeight: '28px',
|
|
|
|
|
+ backgroundColor: '#52c41a',
|
|
|
|
|
+ borderRadius: '50%',
|
|
|
|
|
+ display: 'flex',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ boxShadow: '0 0 0 4px rgba(82, 196, 26, 0.15), 0 0 0 8px rgba(82, 196, 26, 0.08)',
|
|
|
|
|
+ boxSizing: 'border-box',
|
|
|
|
|
+ flexShrink: 0
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <CheckCircleOutlined style={{ fontSize: '20px', color: 'white', display: 'block' }} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
if (status === 'in_progress') {
|
|
if (status === 'in_progress') {
|
|
|
- return <SyncOutlined spin style={{ fontSize: '16px', color: '#1890ff' }} />;
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ width: '28px',
|
|
|
|
|
+ height: '28px',
|
|
|
|
|
+ minWidth: '28px',
|
|
|
|
|
+ minHeight: '28px',
|
|
|
|
|
+ maxWidth: '28px',
|
|
|
|
|
+ maxHeight: '28px',
|
|
|
|
|
+ backgroundColor: '#1890ff',
|
|
|
|
|
+ borderRadius: '50%',
|
|
|
|
|
+ display: 'flex',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ boxShadow: '0 0 0 4px rgba(24, 144, 255, 0.15), 0 0 0 8px rgba(24, 144, 255, 0.08)',
|
|
|
|
|
+ boxSizing: 'border-box',
|
|
|
|
|
+ flexShrink: 0
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <SyncOutlined spin style={{ fontSize: '20px', color: 'white', display: 'block' }} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
- return <ClockCircleOutlined style={{ fontSize: '16px', color: '#d9d9d9' }} />;
|
|
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div style={{
|
|
|
|
|
+ width: '28px',
|
|
|
|
|
+ height: '28px',
|
|
|
|
|
+ minWidth: '28px',
|
|
|
|
|
+ minHeight: '28px',
|
|
|
|
|
+ maxWidth: '28px',
|
|
|
|
|
+ maxHeight: '28px',
|
|
|
|
|
+ backgroundColor: '#d9d9d9',
|
|
|
|
|
+ borderRadius: '50%',
|
|
|
|
|
+ display: 'flex',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
|
|
+ boxShadow: '0 0 0 4px rgba(217, 217, 217, 0.15), 0 0 0 8px rgba(217, 217, 217, 0.08)',
|
|
|
|
|
+ boxSizing: 'border-box',
|
|
|
|
|
+ flexShrink: 0
|
|
|
|
|
+ }}>
|
|
|
|
|
+ <ClockCircleOutlined style={{ fontSize: '20px', color: 'white', display: 'block' }} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 构建 Timeline 数据
|
|
// 构建 Timeline 数据
|
|
@@ -409,35 +466,42 @@ const WorkflowRenderer: React.FC<WorkflowRendererProps> = ({
|
|
|
|
|
|
|
|
const executorInfo = getExecutorInfo(node, node.type || '');
|
|
const executorInfo = getExecutorInfo(node, node.type || '');
|
|
|
const time = node.createTime || node.updateTime;
|
|
const time = node.createTime || node.updateTime;
|
|
|
- const isBranchParent = node.isBranchParent && node.branchNodes && node.branchNodes.length > 1;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 获取主节点uuid(如果存在mainNodeUuid则使用它,否则使用当前节点的uuid)
|
|
|
|
|
+ const mainNodeUuid = (node as any).mainNodeUuid || node.uuid || '';
|
|
|
|
|
+ // 过滤掉主节点,只显示其他分支节点
|
|
|
|
|
+ const otherBranchNodes = node.isBranchParent && node.branchNodes
|
|
|
|
|
+ ? node.branchNodes.filter(branchNode => branchNode.uuid !== mainNodeUuid)
|
|
|
|
|
+ : [];
|
|
|
|
|
+
|
|
|
|
|
+ // 只有当有其他分支节点时才显示展开按钮
|
|
|
|
|
+ const isBranchParent = node.isBranchParent && node.branchNodes && node.branchNodes.length > 1 && otherBranchNodes.length > 0;
|
|
|
const branchParentUuid = (node as any).branchParentUuid || node.parentUuid || node.uuid || '';
|
|
const branchParentUuid = (node as any).branchParentUuid || node.parentUuid || node.uuid || '';
|
|
|
const isExpanded = isBranchParent && expandedBranchGroups.has(branchParentUuid);
|
|
const isExpanded = isBranchParent && expandedBranchGroups.has(branchParentUuid);
|
|
|
const nodeKey = node.uuid || String(node.id || index);
|
|
const nodeKey = node.uuid || String(node.id || index);
|
|
|
const isVisible = visibleNodes.has(nodeKey);
|
|
const isVisible = visibleNodes.has(nodeKey);
|
|
|
|
|
|
|
|
// 构建分支节点内容
|
|
// 构建分支节点内容
|
|
|
- const branchContent = isBranchParent && isExpanded && node.branchNodes ? (
|
|
|
|
|
|
|
+ const branchContent = isBranchParent && isExpanded && otherBranchNodes.length > 0 ? (
|
|
|
<div className="mt-3 space-y-2" style={{ width: '100%' }}>
|
|
<div className="mt-3 space-y-2" style={{ width: '100%' }}>
|
|
|
- {node.branchNodes.map((branchNode, branchIndex) => {
|
|
|
|
|
|
|
+ {otherBranchNodes.map((branchNode, branchIndex) => {
|
|
|
let branchStatus: 'completed' | 'in_progress' | 'pending' = 'pending';
|
|
let branchStatus: 'completed' | 'in_progress' | 'pending' = 'pending';
|
|
|
if (branchNode.approvalStatus === 'approved') {
|
|
if (branchNode.approvalStatus === 'approved') {
|
|
|
branchStatus = 'completed';
|
|
branchStatus = 'completed';
|
|
|
} else if (branchNode.approvalStatus === 'unaudited' || branchNode.approvalStatus === 'pending') {
|
|
} else if (branchNode.approvalStatus === 'unaudited' || branchNode.approvalStatus === 'pending') {
|
|
|
- if (branchIndex > 0) {
|
|
|
|
|
- const prevBranch = node.branchNodes![branchIndex - 1];
|
|
|
|
|
- if (prevBranch && prevBranch.approvalStatus === 'approved') {
|
|
|
|
|
- branchStatus = 'in_progress';
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 检查主节点状态
|
|
|
|
|
+ if (node.approvalStatus === 'approved') {
|
|
|
|
|
+ branchStatus = 'in_progress';
|
|
|
} else {
|
|
} else {
|
|
|
- if (node.approvalStatus === 'approved') {
|
|
|
|
|
|
|
+ // 检查前面的分支节点(包括主节点)是否有已完成的
|
|
|
|
|
+ const allNodesBefore = node.branchNodes!.filter(n => {
|
|
|
|
|
+ const nodeIndex = node.branchNodes!.findIndex(bn => bn.uuid === n.uuid);
|
|
|
|
|
+ const currentIndex = node.branchNodes!.findIndex(bn => bn.uuid === branchNode.uuid);
|
|
|
|
|
+ return nodeIndex < currentIndex;
|
|
|
|
|
+ });
|
|
|
|
|
+ const hasCompletedBefore = allNodesBefore.some(n => n.approvalStatus === 'approved');
|
|
|
|
|
+ if (hasCompletedBefore) {
|
|
|
branchStatus = 'in_progress';
|
|
branchStatus = 'in_progress';
|
|
|
- } else {
|
|
|
|
|
- const hasCompletedBranch = node.branchNodes!.some((n, idx) =>
|
|
|
|
|
- idx < branchIndex && n.approvalStatus === 'approved'
|
|
|
|
|
- );
|
|
|
|
|
- if (hasCompletedBranch) {
|
|
|
|
|
- branchStatus = 'in_progress';
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -448,12 +512,12 @@ const WorkflowRenderer: React.FC<WorkflowRendererProps> = ({
|
|
|
return (
|
|
return (
|
|
|
<div
|
|
<div
|
|
|
key={branchNode.uuid || branchNode.id || branchIndex}
|
|
key={branchNode.uuid || branchNode.id || branchIndex}
|
|
|
- className={`mb-2 p-3 rounded-lg border-l-3 shadow-sm transition-all duration-300 hover:shadow bg-white ${
|
|
|
|
|
|
|
+ className={`mb-2 p-3 rounded-lg border shadow-md transition-all duration-300 hover:shadow-lg ${
|
|
|
branchStatus === 'completed'
|
|
branchStatus === 'completed'
|
|
|
- ? 'border-l-green-500 border-t border-r border-b border-gray-200'
|
|
|
|
|
|
|
+ ? 'bg-emerald-50 border-emerald-200'
|
|
|
: branchStatus === 'in_progress'
|
|
: branchStatus === 'in_progress'
|
|
|
- ? 'border-l-blue-500 border-t border-r border-b border-gray-200'
|
|
|
|
|
- : 'border-l-gray-400 border-t border-r border-b border-gray-200'
|
|
|
|
|
|
|
+ ? 'bg-sky-50 border-sky-200'
|
|
|
|
|
+ : 'bg-gray-50 border-gray-200'
|
|
|
}`}
|
|
}`}
|
|
|
style={{
|
|
style={{
|
|
|
animation: isVisible ? 'fadeInUp 0.5s ease-out' : 'none',
|
|
animation: isVisible ? 'fadeInUp 0.5s ease-out' : 'none',
|
|
@@ -501,7 +565,7 @@ const WorkflowRenderer: React.FC<WorkflowRendererProps> = ({
|
|
|
return {
|
|
return {
|
|
|
key: nodeKey,
|
|
key: nodeKey,
|
|
|
dot: getTimelineDot(status),
|
|
dot: getTimelineDot(status),
|
|
|
- color: getTimelineColor(status),
|
|
|
|
|
|
|
+ color: undefined,
|
|
|
children: (
|
|
children: (
|
|
|
<div
|
|
<div
|
|
|
className={`transition-all duration-500 ${
|
|
className={`transition-all duration-500 ${
|
|
@@ -513,12 +577,12 @@ const WorkflowRenderer: React.FC<WorkflowRendererProps> = ({
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
<div
|
|
<div
|
|
|
- className={`mb-3 p-4 rounded-lg border-l-4 shadow-sm transition-all duration-300 hover:shadow-md bg-white ${
|
|
|
|
|
|
|
+ className={`mb-3 p-4 rounded-lg border shadow-md transition-all duration-300 hover:shadow-lg ${
|
|
|
status === 'completed'
|
|
status === 'completed'
|
|
|
- ? 'border-l-green-500 border-t border-r border-b border-gray-200'
|
|
|
|
|
|
|
+ ? 'bg-emerald-50 border-emerald-200'
|
|
|
: status === 'in_progress'
|
|
: status === 'in_progress'
|
|
|
- ? 'border-l-blue-500 border-t border-r border-b border-gray-200'
|
|
|
|
|
- : 'border-l-gray-400 border-t border-r border-b border-gray-200'
|
|
|
|
|
|
|
+ ? 'bg-sky-50 border-sky-200'
|
|
|
|
|
+ : 'bg-gray-50 border-gray-200'
|
|
|
}`}
|
|
}`}
|
|
|
style={{
|
|
style={{
|
|
|
maxWidth: '100%',
|
|
maxWidth: '100%',
|
|
@@ -616,24 +680,93 @@ const WorkflowRenderer: React.FC<WorkflowRendererProps> = ({
|
|
|
max-width: 280px;
|
|
max-width: 280px;
|
|
|
}
|
|
}
|
|
|
.workflow-timeline .ant-timeline-item-tail {
|
|
.workflow-timeline .ant-timeline-item-tail {
|
|
|
- border-left: 2px solid #e8e8e8;
|
|
|
|
|
|
|
+ border-left: 3px solid #1890ff;
|
|
|
}
|
|
}
|
|
|
.workflow-timeline .ant-timeline-item-head {
|
|
.workflow-timeline .ant-timeline-item-head {
|
|
|
- width: 16px;
|
|
|
|
|
- height: 16px;
|
|
|
|
|
- border-width: 2px;
|
|
|
|
|
|
|
+ width: 14px;
|
|
|
|
|
+ height: 14px;
|
|
|
|
|
+ border-width: 0;
|
|
|
|
|
+ background-color: #1890ff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
|
|
|
}
|
|
}
|
|
|
.workflow-timeline .ant-timeline-item-head-green {
|
|
.workflow-timeline .ant-timeline-item-head-green {
|
|
|
- border-color: #52c41a;
|
|
|
|
|
- background-color: #52c41a;
|
|
|
|
|
|
|
+ width: 28px !important;
|
|
|
|
|
+ height: 28px !important;
|
|
|
|
|
+ min-width: 28px !important;
|
|
|
|
|
+ min-height: 28px !important;
|
|
|
|
|
+ max-width: 28px !important;
|
|
|
|
|
+ max-height: 28px !important;
|
|
|
|
|
+ background-color: #52c41a !important;
|
|
|
|
|
+ border-radius: 50% !important;
|
|
|
|
|
+ border: none !important;
|
|
|
|
|
+ box-shadow: 0 0 0 4px rgba(82, 196, 26, 0.15), 0 0 0 8px rgba(82, 196, 26, 0.08) !important;
|
|
|
|
|
+ display: flex !important;
|
|
|
|
|
+ align-items: center !important;
|
|
|
|
|
+ justify-content: center !important;
|
|
|
|
|
+ box-sizing: border-box !important;
|
|
|
|
|
+ flex-shrink: 0 !important;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ .workflow-timeline .ant-timeline-item-head-green .anticon,
|
|
|
|
|
+ .workflow-timeline .ant-timeline-item-head-green svg {
|
|
|
|
|
+ color: white !important;
|
|
|
|
|
+ fill: white !important;
|
|
|
|
|
+ font-size: 20px !important;
|
|
|
|
|
+ width: 20px !important;
|
|
|
|
|
+ height: 20px !important;
|
|
|
}
|
|
}
|
|
|
.workflow-timeline .ant-timeline-item-head-blue {
|
|
.workflow-timeline .ant-timeline-item-head-blue {
|
|
|
- border-color: #1890ff;
|
|
|
|
|
- background-color: #1890ff;
|
|
|
|
|
|
|
+ width: 28px !important;
|
|
|
|
|
+ height: 28px !important;
|
|
|
|
|
+ min-width: 28px !important;
|
|
|
|
|
+ min-height: 28px !important;
|
|
|
|
|
+ max-width: 28px !important;
|
|
|
|
|
+ max-height: 28px !important;
|
|
|
|
|
+ background-color: #1890ff !important;
|
|
|
|
|
+ border-radius: 50% !important;
|
|
|
|
|
+ border: none !important;
|
|
|
|
|
+ box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.15), 0 0 0 8px rgba(24, 144, 255, 0.08) !important;
|
|
|
|
|
+ display: flex !important;
|
|
|
|
|
+ align-items: center !important;
|
|
|
|
|
+ justify-content: center !important;
|
|
|
|
|
+ box-sizing: border-box !important;
|
|
|
|
|
+ flex-shrink: 0 !important;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ .workflow-timeline .ant-timeline-item-head-blue .anticon,
|
|
|
|
|
+ .workflow-timeline .ant-timeline-item-head-blue svg {
|
|
|
|
|
+ color: white !important;
|
|
|
|
|
+ fill: white !important;
|
|
|
|
|
+ font-size: 20px !important;
|
|
|
|
|
+ width: 20px !important;
|
|
|
|
|
+ height: 20px !important;
|
|
|
}
|
|
}
|
|
|
.workflow-timeline .ant-timeline-item-head-gray {
|
|
.workflow-timeline .ant-timeline-item-head-gray {
|
|
|
- border-color: #d9d9d9;
|
|
|
|
|
- background-color: #fff;
|
|
|
|
|
|
|
+ width: 28px !important;
|
|
|
|
|
+ height: 28px !important;
|
|
|
|
|
+ min-width: 28px !important;
|
|
|
|
|
+ min-height: 28px !important;
|
|
|
|
|
+ max-width: 28px !important;
|
|
|
|
|
+ max-height: 28px !important;
|
|
|
|
|
+ background-color: #d9d9d9 !important;
|
|
|
|
|
+ border-radius: 50% !important;
|
|
|
|
|
+ border: none !important;
|
|
|
|
|
+ box-shadow: 0 0 0 4px rgba(217, 217, 217, 0.15), 0 0 0 8px rgba(217, 217, 217, 0.08) !important;
|
|
|
|
|
+ display: flex !important;
|
|
|
|
|
+ align-items: center !important;
|
|
|
|
|
+ justify-content: center !important;
|
|
|
|
|
+ box-sizing: border-box !important;
|
|
|
|
|
+ flex-shrink: 0 !important;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ .workflow-timeline .ant-timeline-item-head-gray .anticon,
|
|
|
|
|
+ .workflow-timeline .ant-timeline-item-head-gray svg {
|
|
|
|
|
+ color: white !important;
|
|
|
|
|
+ fill: white !important;
|
|
|
|
|
+ font-size: 20px !important;
|
|
|
|
|
+ width: 20px !important;
|
|
|
|
|
+ height: 20px !important;
|
|
|
}
|
|
}
|
|
|
`}</style>
|
|
`}</style>
|
|
|
<Timeline
|
|
<Timeline
|