From 0713c2c83e436cf1892dc301c9f44d17f75e6213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=9A=E8=BE=B0=E5=85=88=E7=94=9F?= Date: Thu, 5 Dec 2024 21:19:23 +0800 Subject: [PATCH] 1 --- .../Designer/components/NodePanel/index.css | 67 +++++++ .../components/NodePanel/index.module.less | 52 +++-- .../Designer/components/NodePanel/index.tsx | 187 ++++++++---------- .../Definition/Designer/index.module.less | 46 +++-- .../Designer/index.module.less.d.ts | 7 +- .../Workflow/Definition/Designer/index.tsx | 14 +- .../Workflow/Definition/Designer/service.ts | 31 +++ .../pages/Workflow/Definition/constants.ts | 17 ++ .../src/pages/Workflow/Definition/index.tsx | 1 - .../src/pages/Workflow/Definition/service.ts | 129 ++++++++++++ .../src/pages/Workflow/Definition/types.ts | 15 ++ frontend/src/types/less.d.ts | 4 + frontend/tsconfig.json | 23 ++- 13 files changed, 412 insertions(+), 181 deletions(-) create mode 100644 frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.css create mode 100644 frontend/src/pages/Workflow/Definition/Designer/service.ts create mode 100644 frontend/src/pages/Workflow/Definition/constants.ts create mode 100644 frontend/src/pages/Workflow/Definition/service.ts create mode 100644 frontend/src/pages/Workflow/Definition/types.ts create mode 100644 frontend/src/types/less.d.ts diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.css b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.css new file mode 100644 index 00000000..d79371bd --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.css @@ -0,0 +1,67 @@ +.node-panel { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.node-panel-loading { + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.node-panel-content { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 12px; + padding: 12px; + overflow-y: auto; +} + +.node-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 12px; + border: 1px solid #e8e8e8; + border-radius: 4px; + cursor: move; + transition: all 0.3s; + background: white; +} + +.node-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.node-icon { + font-size: 24px; + margin-bottom: 8px; +} + +.node-name { + font-size: 12px; + text-align: center; + color: #333; + line-height: 1.5; +} + +.node-tabs { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.node-tabs :global(.ant-tabs-content) { + flex: 1; + overflow: hidden; +} + +.node-tabs :global(.ant-tabs-tabpane) { + height: 100%; + overflow: hidden; +} \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.module.less b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.module.less index 525ce3a1..dfa5c099 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.module.less +++ b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.module.less @@ -1,7 +1,7 @@ -.nodeTabs { - height: 100%; - - :global { +:global { + .workflow-node-tabs { + height: 100%; + .ant-tabs { height: 100%; @@ -68,24 +68,24 @@ color: #999; } } -} -.nodeCard { - margin-bottom: 16px; - cursor: move; - border-radius: 4px; - transition: all 0.3s; - background: #fff; - border: 1px solid #f0f0f0; - padding: 12px; + .workflow-node-card { + margin-bottom: 16px; + cursor: move; + border-radius: 4px; + transition: all 0.3s; + background: #fff; + border: 1px solid #f0f0f0; + padding: 12px; - &:hover { - border-color: #1890ff; - box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15); - transform: translateY(-1px); + &:hover { + border-color: #1890ff; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15); + transform: translateY(-1px); + } } - .nodeIcon { + .workflow-node-icon { width: 40px; height: 40px; border-radius: 8px; @@ -100,17 +100,17 @@ .anticon { transition: all 0.3s; } - } - &:hover .nodeIcon { - transform: scale(1.1); - - .anticon { + &:hover { transform: scale(1.1); + + .anticon { + transform: scale(1.1); + } } } - .nodeName { + .workflow-node-name { font-size: 14px; color: #333; text-align: center; @@ -118,8 +118,4 @@ margin: 0; font-weight: 500; } - - &:active { - transform: scale(0.98); - } } \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.tsx index eb8b85f2..dfa96d73 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/components/NodePanel/index.tsx @@ -1,121 +1,98 @@ -import React, {useEffect, useState} from 'react'; -import {Card, Empty, Spin, Tabs, Tooltip} from 'antd'; -import {NodeCategory, NodeType} from '../../types'; -import {getNodeTypes} from '../../service'; -import * as Icons from '@ant-design/icons'; -import styles from './NodePanel.module.less'; +import React, { useEffect, useState } from 'react'; +import { Tabs, Spin } from 'antd'; +import { NodeType, getNodeTypes } from '../../service'; +import './index.css'; interface NodePanelProps { - onNodeDragStart: (nodeType: NodeType) => void; + onNodeDragStart: (nodeType: NodeType) => void; } -const NodePanel: React.FC = ({onNodeDragStart}) => { - const [loading, setLoading] = useState(false); - const [nodeTypes, setNodeTypes] = useState([]); +const NodePanel: React.FC = ({ onNodeDragStart }) => { + const [nodeTypes, setNodeTypes] = useState([]); + const [loading, setLoading] = useState(false); - // 获取节点类型列表 + useEffect(() => { const fetchNodeTypes = async () => { - setLoading(true); - try { - const response = await getNodeTypes({enabled: true}); - if (response) { - setNodeTypes(response); - } - } finally { - setLoading(false); + setLoading(true); + try { + const response = await getNodeTypes({ enabled: true }); + if (response) { + setNodeTypes(response); } + } catch (error) { + console.error('获取节点类型失败:', error); + } finally { + setLoading(false); + } }; - useEffect(() => { - fetchNodeTypes(); - }, []); + fetchNodeTypes(); + }, []); - // 按分类分组节点类型 - const groupedNodeTypes = nodeTypes.reduce((acc, nodeType) => { - const category = nodeType.category; - if (!acc[category]) { - acc[category] = []; - } - acc[category].push(nodeType); - return acc; - }, {} as Record); - - // 渲染图标 - const renderIcon = (iconName: string) => { - // @ts-ignore - const Icon = Icons[iconName]; - return Icon ? : null; - }; - - // 渲染节点类型卡片 - const renderNodeTypeCard = (nodeType: NodeType) => ( - - { - e.dataTransfer.setData('nodeType', JSON.stringify(nodeType)); - onNodeDragStart(nodeType); - }} - > -
- {renderIcon(nodeType.icon)} -
-
{nodeType.name}
-
-
- ); - - const categoryIcons = { - [NodeCategory.TASK]: renderIcon('CodeOutlined'), - [NodeCategory.EVENT]: renderIcon('ThunderboltOutlined'), - [NodeCategory.GATEWAY]: renderIcon('ForkOutlined') - }; - - const categoryLabels = { - [NodeCategory.TASK]: '任务节点', - [NodeCategory.EVENT]: '事件节点', - [NodeCategory.GATEWAY]: '网关节点' - }; - - const tabItems = Object.values(NodeCategory).map(category => ({ - key: category, - label: ( - - {categoryIcons[category]} {categoryLabels[category]} - - ({groupedNodeTypes[category]?.length || 0}) - - - ), - children: groupedNodeTypes[category]?.map(renderNodeTypeCard) || ( - - ) - })); - - if (loading) { - return ( -
- -
- ); + // 按类别分组节点类型 + const groupedNodeTypes = nodeTypes.reduce((acc, nodeType) => { + const category = nodeType.category; + if (!acc[category]) { + acc[category] = []; } + acc[category].push(nodeType); + return acc; + }, {} as Record); + // 将分组转换为 Tabs 项 + const tabItems = Object.entries(groupedNodeTypes).map(([category, types]) => ({ + key: category, + label: getCategoryLabel(category), + children: ( +
+ {types.map((nodeType) => ( +
{ + e.dataTransfer.setData('node-type', JSON.stringify(nodeType)); + onNodeDragStart(nodeType); + }} + style={{ borderColor: nodeType.color }} + > +
+ +
+
{nodeType.name}
+
+ ))} +
+ ), + })); + + if (loading) { return ( - +
+ +
); + } + + return ( +
+ +
+ ); }; -export default NodePanel; \ No newline at end of file +// 获取类别标签 +const getCategoryLabel = (category: string) => { + const labels: Record = { + TASK: '任务节点', + EVENT: '事件节点', + GATEWAY: '网关节点', + }; + return labels[category] || category; +}; + +export default NodePanel; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.module.less b/frontend/src/pages/Workflow/Definition/Designer/index.module.less index 3cc6f143..ffe2f017 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.module.less +++ b/frontend/src/pages/Workflow/Definition/Designer/index.module.less @@ -1,30 +1,28 @@ -.container { - height: calc(100vh - 180px); - border: 1px solid #f0f0f0; - border-radius: 2px; - background: #fff; - - .sider { - border-right: 1px solid #f0f0f0; - background: #fff; - - .nodePanel { - padding: 16px; - height: 100%; - overflow-y: auto; +:global { + .workflow-designer { + &-container { + height: calc(100vh - 170px); + background: #fff; + border-radius: 2px; } - } - .content { - position: relative; - background: #fafafa; + &-sider { + background: #fff; + border-right: 1px solid #e8e8e8; + } - .graph { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + &-content { + position: relative; + background: #fafafa; + padding: 16px; + } + + &-graph { + width: 100%; + height: 100%; + background: #fff; + border: 1px solid #e8e8e8; + border-radius: 2px; } } } \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.module.less.d.ts b/frontend/src/pages/Workflow/Definition/Designer/index.module.less.d.ts index ae182ace..364c86e2 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.module.less.d.ts +++ b/frontend/src/pages/Workflow/Definition/Designer/index.module.less.d.ts @@ -1,14 +1,13 @@ -declare namespace IndexModuleLessNamespace { - export interface IIndexModuleLess { +declare namespace DesignerModuleLessNamespace { + export interface IDesignerModuleLess { container: string; sider: string; - nodePanel: string; content: string; graph: string; } } declare module '*.less' { - const content: IndexModuleLessNamespace.IIndexModuleLess; + const content: DesignerModuleLessNamespace.IDesignerModuleLess; export default content; } \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/Designer/index.tsx b/frontend/src/pages/Workflow/Definition/Designer/index.tsx index 3d885aca..82d5087a 100644 --- a/frontend/src/pages/Workflow/Definition/Designer/index.tsx +++ b/frontend/src/pages/Workflow/Definition/Designer/index.tsx @@ -3,11 +3,11 @@ import {useNavigate, useParams} from 'react-router-dom'; import {Button, Card, Layout, message, Space, Spin} from 'antd'; import {ArrowLeftOutlined, SaveOutlined} from '@ant-design/icons'; import {getDefinition, updateDefinition} from '../../service'; -import {WorkflowDefinition, WorkflowStatus, NodeType} from '../../types'; +import {NodeType, WorkflowDefinition, WorkflowStatus} from '../../../Workflow/types'; import {Graph} from '@antv/x6'; import '@antv/x6-react-shape'; -import styles from './index.module.less'; -import NodePanel from './NodePanel'; +import './index.module.less'; +import NodePanel from './components/NodePanel'; const {Sider, Content} = Layout; @@ -220,12 +220,12 @@ const FlowDesigner: React.FC = () => { } > - - + + - -
+ +
diff --git a/frontend/src/pages/Workflow/Definition/Designer/service.ts b/frontend/src/pages/Workflow/Definition/Designer/service.ts new file mode 100644 index 00000000..d1896ee2 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/Designer/service.ts @@ -0,0 +1,31 @@ +import request from '@/utils/request'; + +export interface NodeExecutor { + code: string; + name: string; + description: string; + configSchema: Record; +} + +export interface NodeType { + id: number; + code: string; + name: string; + category: 'TASK' | 'EVENT' | 'GATEWAY'; + description: string; + enabled: boolean; + icon: string; + color: string; + executors: NodeExecutor[]; + createTime: string; + updateTime: string; +} + +export interface NodeTypeQuery { + enabled?: boolean; + category?: 'TASK' | 'EVENT' | 'GATEWAY'; +} + +// 获取节点类型列表 +export const getNodeTypes = (params?: NodeTypeQuery) => + request.get('/api/v1/node-types', { params }); \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/constants.ts b/frontend/src/pages/Workflow/Definition/constants.ts new file mode 100644 index 00000000..c77d90c4 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/constants.ts @@ -0,0 +1,17 @@ +import { NodeCategory } from './types'; + +// 节点分类配置 +export const NODE_CATEGORY_CONFIG = { + [NodeCategory.TASK]: { + icon: 'CodeOutlined', + label: '任务节点', + }, + [NodeCategory.EVENT]: { + icon: 'ThunderboltOutlined', + label: '事件节点', + }, + [NodeCategory.GATEWAY]: { + icon: 'ForkOutlined', + label: '网关节点', + }, +} as const; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/index.tsx b/frontend/src/pages/Workflow/Definition/index.tsx index 44f3c4e9..a2e2d19e 100644 --- a/frontend/src/pages/Workflow/Definition/index.tsx +++ b/frontend/src/pages/Workflow/Definition/index.tsx @@ -10,7 +10,6 @@ import { publishDefinition } from '../service'; import { - CreateWorkflowDefinitionRequest, WorkflowDefinition, WorkflowStatus, WorkflowDefinitionBase diff --git a/frontend/src/pages/Workflow/Definition/service.ts b/frontend/src/pages/Workflow/Definition/service.ts new file mode 100644 index 00000000..aaced46e --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/service.ts @@ -0,0 +1,129 @@ +import { NodeCategory, NodeType } from '../types'; + +// Mock 节点类型数据 +const mockNodeTypes: NodeType[] = [ + { + id: 1, + code: 'start', + name: '开始节点', + color: '#67C23A', + icon: 'PlayCircleOutlined', + category: NodeCategory.EVENT, + description: '流程的开始节点', + enabled: true, + executors: [], + configSchema: '{}', + defaultConfig: '{}', + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + }, + { + id: 2, + code: 'end', + name: '结束节点', + color: '#F56C6C', + icon: 'StopOutlined', + category: NodeCategory.EVENT, + description: '流程的结束节点', + enabled: true, + executors: [], + configSchema: '{}', + defaultConfig: '{}', + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + }, + { + id: 3, + code: 'approval', + name: '审批节点', + color: '#409EFF', + icon: 'AuditOutlined', + category: NodeCategory.TASK, + description: '人工审批节点', + enabled: true, + executors: [], + configSchema: '{}', + defaultConfig: '{}', + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + }, + { + id: 4, + code: 'script', + name: '脚本节点', + color: '#909399', + icon: 'CodeOutlined', + category: NodeCategory.TASK, + description: '执行自定义脚本', + enabled: true, + executors: [], + configSchema: '{}', + defaultConfig: '{}', + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + }, + { + id: 5, + code: 'webhook', + name: 'Webhook', + color: '#9C27B0', + icon: 'ApiOutlined', + category: NodeCategory.TASK, + description: '调用外部 Webhook', + enabled: true, + executors: [], + configSchema: '{}', + defaultConfig: '{}', + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + }, + { + id: 6, + code: 'exclusive', + name: '排他网关', + color: '#FF9800', + icon: 'ForkOutlined', + category: NodeCategory.GATEWAY, + description: '根据条件选择一条分支执行', + enabled: true, + executors: [], + configSchema: '{}', + defaultConfig: '{}', + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + }, + { + id: 7, + code: 'parallel', + name: '并行网关', + color: '#795548', + icon: 'PartitionOutlined', + category: NodeCategory.GATEWAY, + description: '同时执行所有分支', + enabled: true, + executors: [], + configSchema: '{}', + defaultConfig: '{}', + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + } +]; + +interface GetNodeTypesParams { + enabled?: boolean; +} + +// 获取节点类型列表 +export const getNodeTypes = async (params?: GetNodeTypesParams): Promise => { + // 模拟 API 延迟 + await new Promise(resolve => setTimeout(resolve, 500)); + + let result = [...mockNodeTypes]; + + // 根据启用状态筛选 + if (params?.enabled !== undefined) { + result = result.filter(node => node.enabled === params.enabled); + } + + return result; +}; \ No newline at end of file diff --git a/frontend/src/pages/Workflow/Definition/types.ts b/frontend/src/pages/Workflow/Definition/types.ts new file mode 100644 index 00000000..14d713e7 --- /dev/null +++ b/frontend/src/pages/Workflow/Definition/types.ts @@ -0,0 +1,15 @@ +export enum NodeCategory { + TASK = 'TASK', + EVENT = 'EVENT', + GATEWAY = 'GATEWAY' +} + +export interface NodeType { + code: string; + name: string; + color: string; + icon: string; + category: NodeCategory; + description?: string; + enabled?: boolean; +} \ No newline at end of file diff --git a/frontend/src/types/less.d.ts b/frontend/src/types/less.d.ts new file mode 100644 index 00000000..5dde3ce6 --- /dev/null +++ b/frontend/src/types/less.d.ts @@ -0,0 +1,4 @@ +declare module '*.less' { + const classes: { readonly [key: string]: string }; + export default classes; +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a9baa548..37597889 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,26 +1,25 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ESNext", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, + "typeRoots": ["./node_modules/@types", "./src/types"], "baseUrl": ".", "paths": { "@/*": ["src/*"] - }, - "types": ["@antv/x6"], - "allowSyntheticDefaultImports": true + } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }]