feat(web): Add Workflow
This commit is contained in:
19
web/src/views/Workflow/components/Nodes/AddNode.tsx
Normal file
19
web/src/views/Workflow/components/Nodes/AddNode.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
|
||||
const AddNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {}
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:group rb:relative rb:h-10 rb:w-30 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
||||
})}>
|
||||
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
||||
{data.icon} {data.label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNode;
|
||||
155
web/src/views/Workflow/components/Nodes/ConditionNode.tsx
Normal file
155
web/src/views/Workflow/components/Nodes/ConditionNode.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from 'antd'
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
|
||||
const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {};
|
||||
|
||||
const addPort = (e: React.MouseEvent) => {
|
||||
if (!node || !node.addPort) return;
|
||||
e.stopPropagation();
|
||||
|
||||
const currentPorts = node.getPorts();
|
||||
const totalPorts = currentPorts.length;
|
||||
|
||||
// 如果没有端口,添加第一个端口和ELSE端口
|
||||
if (totalPorts === 0) {
|
||||
// 添加第一个ELIF端口
|
||||
node.addPort({
|
||||
id: 'elif_1',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELIF 1',
|
||||
},
|
||||
},
|
||||
});
|
||||
// 添加ELSE端口
|
||||
node.addPort({
|
||||
id: 'else',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELSE',
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果只有一个端口,确保它是ELSE,然后在之前添加ELIF
|
||||
if (totalPorts === 1) {
|
||||
const existingPort = currentPorts[0];
|
||||
|
||||
// 如果现有端口不是ELSE,先移除它
|
||||
if (node.removePort && existingPort.id !== 'else') {
|
||||
node.removePort(existingPort.id as string);
|
||||
|
||||
// 添加ELIF端口
|
||||
node.addPort({
|
||||
id: 'elif_1',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELIF 1',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 添加或确保存在ELSE端口
|
||||
if (existingPort.id !== 'else') {
|
||||
node.addPort({
|
||||
id: 'else',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELSE',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取最后一个端口,确保它是ELSE
|
||||
let lastPort = currentPorts[totalPorts - 1];
|
||||
|
||||
// 如果最后一个端口不是ELSE,先移除它
|
||||
if (node.removePort && lastPort.id !== 'else') {
|
||||
node.removePort(lastPort.id as string);
|
||||
|
||||
// 添加ELSE端口作为最后一个
|
||||
node.addPort({
|
||||
id: 'else',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELSE',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 更新currentPorts和totalPorts
|
||||
const updatedPorts = node.getPorts();
|
||||
const updatedTotal = updatedPorts.length;
|
||||
lastPort = updatedPorts[updatedTotal - 1];
|
||||
}
|
||||
|
||||
// 计算新的ELIF端口数量(最后一个是ELSE,不算在内)
|
||||
const elifCount = totalPorts - 1;
|
||||
const newElifCount = elifCount + 1;
|
||||
|
||||
// 如果有removePort方法,先移除最后一个端口(ELSE),添加新的ELIF端口,再添加回ELSE端口
|
||||
if (node.removePort) {
|
||||
// 移除最后一个端口(ELSE)
|
||||
node.removePort(lastPort.id as string);
|
||||
|
||||
// 添加新的ELIF端口在倒数第二个位置
|
||||
node.addPort({
|
||||
id: `elif_${newElifCount}`,
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: `ELIF ${newElifCount}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 添加回ELSE端口
|
||||
node.addPort({
|
||||
id: 'else',
|
||||
group: 'right',
|
||||
attrs: {
|
||||
text: {
|
||||
text: 'ELSE',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// const removeElif = (e: React.MouseEvent) => {
|
||||
// e.stopPropagation();
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className={clsx(`rb:border rb:rounded-[12px] rb:relative rb:min-w-[200px] rb:min-h-[120px] rb:p-2`, {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-orange-50 rb:text-gray-700': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-[#FFFFFF] rb:text-[#374151]': !data.isSelected
|
||||
})}>
|
||||
|
||||
<Button onClick={addPort}>+ 添加 ELIF</Button>
|
||||
{/* 标题区域 */}
|
||||
<div className="rb:absolute rb:-top-3 rb:left-2 rb:bg-blue-500 rb:rounded-2xl rb:px-3 rb:py-1 rb:flex rb:items-center rb:gap-1.5 rb:text-white rb:text-xs rb:font-bold rb:z-10">
|
||||
<div className="rb:w-4 rb:h-4 rb:bg-white rb:rounded rb:flex rb:items-center rb:justify-center rb:text-blue-500 rb:text-[10px]">
|
||||
🔀
|
||||
</div>
|
||||
条件分支
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConditionNode;
|
||||
19
web/src/views/Workflow/components/Nodes/GroupStartNode.tsx
Normal file
19
web/src/views/Workflow/components/Nodes/GroupStartNode.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
|
||||
const GroupStartNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {}
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:group rb:relative rb:h-10 rb:w-20 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
||||
})}>
|
||||
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
||||
{data.icon} {data.label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupStartNode;
|
||||
98
web/src/views/Workflow/components/Nodes/IterationNode.tsx
Normal file
98
web/src/views/Workflow/components/Nodes/IterationNode.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Dropdown } from 'antd';
|
||||
import { SmallDashOutlined } from '@ant-design/icons';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { graphNodeLibrary } from '../../constant';
|
||||
|
||||
interface NodeData {
|
||||
isSelected?: boolean;
|
||||
type?: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
parentId?: string;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
const IterationNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const data = node.getData() as NodeData;
|
||||
|
||||
useEffect(() => {
|
||||
initNodes()
|
||||
}, [])
|
||||
|
||||
const initNodes = () => {
|
||||
// 添加默认子节点
|
||||
const parentBBox = node.getBBox();
|
||||
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
||||
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
||||
|
||||
const childNode1 = graph.addNode({
|
||||
...graphNodeLibrary.groupStart,
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'default',
|
||||
label: '开始',
|
||||
// icon: '📌',
|
||||
parentId: node.id,
|
||||
isDefault: true // 标记为默认节点,不可删除
|
||||
},
|
||||
});
|
||||
const childNode2 = graph.addNode({
|
||||
...graphNodeLibrary.addStart,
|
||||
x: centerX + 150,
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'default',
|
||||
label: '添加节点',
|
||||
icon: '+',
|
||||
parentId: node.id,
|
||||
},
|
||||
});
|
||||
node.addChild(childNode1)
|
||||
node.addChild(childNode2)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:group rb:border-2 rb:border-dashed rb:rounded-xl rb:relative rb:min-w-75 rb:min-h-50 rb:p-4', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data?.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data?.isSelected
|
||||
})}>
|
||||
{/* 标题区域 */}
|
||||
<div className="rb:absolute rb:-top-3 rb:left-4 rb:bg-[#10b981] rb:rounded-[20px] rb:p-[8px_16px] rb:flex rb:items-center rb:gap-2 rb:text-white rb:text-[14px] rb:font-bold rb:z-10">
|
||||
<div className="rb:w-5 rb:h-5 rb:bg-[#FFFFFF] rb:rounded-sm rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:text-[#10b981]">
|
||||
🔁
|
||||
</div>
|
||||
迭代
|
||||
</div>
|
||||
<Dropdown
|
||||
menu={{items: [
|
||||
{
|
||||
key: '1',
|
||||
label: '删除',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '复制',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '删除',
|
||||
}
|
||||
]}}
|
||||
>
|
||||
<SmallDashOutlined
|
||||
className={clsx("rb:cursor-pointer rb:right-1 rb:top-1 rb:invisible rb:absolute rb:group-hover:visible", {
|
||||
'rb:visible': data.isSelected
|
||||
})}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
{/* 画布内容区域 */}
|
||||
<div className="rb:mt-6 rb:min-h-37.5 rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IterationNode;
|
||||
98
web/src/views/Workflow/components/Nodes/LoopNode.tsx
Normal file
98
web/src/views/Workflow/components/Nodes/LoopNode.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Dropdown } from 'antd';
|
||||
import { SmallDashOutlined } from '@ant-design/icons';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { graphNodeLibrary } from '../../constant';
|
||||
|
||||
interface NodeData {
|
||||
isSelected?: boolean;
|
||||
type?: string;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
parentId?: string;
|
||||
isGroup?: boolean;
|
||||
}
|
||||
|
||||
const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const data = node.getData() as NodeData;
|
||||
|
||||
useEffect(() => {
|
||||
initNodes()
|
||||
}, [])
|
||||
|
||||
const initNodes = () => {
|
||||
// 添加默认子节点
|
||||
const parentBBox = node.getBBox();
|
||||
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
||||
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
||||
|
||||
const childNode1 = graph.addNode({
|
||||
...graphNodeLibrary.groupStart,
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'default',
|
||||
label: '开始',
|
||||
// icon: '📌',
|
||||
parentId: node.id,
|
||||
isDefault: true // 标记为默认节点,不可删除
|
||||
},
|
||||
});
|
||||
const childNode2 = graph.addNode({
|
||||
...graphNodeLibrary.addStart,
|
||||
x: centerX + 150,
|
||||
y: centerY,
|
||||
data: {
|
||||
type: 'default',
|
||||
label: '添加节点',
|
||||
icon: '+',
|
||||
parentId: node.id,
|
||||
},
|
||||
});
|
||||
node.addChild(childNode1)
|
||||
node.addChild(childNode2)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:group rb:border-2 rb:border-dashed rb:rounded-[12px] rb:relative rb:min-w-[300px] rb:min-h-[200px] rb:p-4', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data?.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data?.isSelected
|
||||
})}>
|
||||
{/* 标题区域 */}
|
||||
<div className="rb:absolute rb:-top-3 rb:left-4 rb:bg-[#10b981] rb:rounded-[20px] rb:p-[8px_16px] rb:flex rb:items-center rb:gap-2 rb:text-white rb:text-[14px] rb:font-bold rb:z-10">
|
||||
<div className="rb:w-5 rb:h-5 rb:bg-[#FFFFFF] rb:rounded-sm rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:text-[#10b981]">
|
||||
♻️
|
||||
</div>
|
||||
循环
|
||||
</div>
|
||||
<Dropdown
|
||||
menu={{items: [
|
||||
{
|
||||
key: '1',
|
||||
label: '删除',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: '复制',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: '删除',
|
||||
}
|
||||
]}}
|
||||
>
|
||||
<SmallDashOutlined
|
||||
className={clsx("rb:cursor-pointer rb:right-1 rb:top-1 rb:invisible rb:absolute rb:group-hover:visible", {
|
||||
'rb:visible': data.isSelected
|
||||
})}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
{/* 画布内容区域 */}
|
||||
<div className="rb:mt-6 rb:min-h-[150px] rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoopNode;
|
||||
31
web/src/views/Workflow/components/Nodes/NormalNode.tsx
Normal file
31
web/src/views/Workflow/components/Nodes/NormalNode.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
|
||||
const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {}
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-16 rb:w-60 rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
||||
<img src={data.icon} className="rb:w-5 rb:h-5" />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => {}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-1.5">{t('workflow.clickToConfigure')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NormalNode;
|
||||
Reference in New Issue
Block a user