# 前端技术设计文档 **版本**: v1.0 **关联**: 01-架构总览.md --- ## 一、技术栈详细说明 ### 1.1 核心依赖 ```json { "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.0.0", "reactflow": "^11.10.0", "antd": "^5.12.0", "@ant-design/icons": "^5.2.6", "zustand": "^4.4.7", "axios": "^1.6.2", "@monaco-editor/react": "^4.6.0", "dayjs": "^1.11.10" }, "devDependencies": { "@vitejs/plugin-react": "^4.2.0", "vite": "^5.0.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "eslint": "^8.55.0", "prettier": "^3.1.1" } } ``` ### 1.2 项目结构 ``` frontend/ ├── src/ │ ├── api/ # API调用 │ │ ├── workflow.ts # 工作流API │ │ ├── nodeType.ts # 节点类型API │ │ └── task.ts # 审批任务API │ │ │ ├── components/ # 通用组件 │ │ ├── WorkflowEditor/ # 工作流编辑器(核心)⭐ │ │ │ ├── index.tsx │ │ │ ├── Canvas.tsx # ReactFlow画布 │ │ │ ├── NodePalette.tsx # 节点面板 │ │ │ ├── CustomNode.tsx # 自定义节点样式 │ │ │ └── MiniMap.tsx # 缩略图 │ │ │ │ │ ├── NodeConfigPanel/ # 节点配置面板(核心)⭐⭐ │ │ │ ├── index.tsx │ │ │ ├── FieldMappingSelector.tsx # 字段映射选择器⭐⭐⭐ │ │ │ ├── ExpressionInput.tsx # 表达式输入框 │ │ │ ├── CodeEditor.tsx # 代码编辑器 │ │ │ └── DynamicForm.tsx # 动态表单 │ │ │ │ │ ├── ExecutionViewer/ # 执行查看器 │ │ │ ├── index.tsx │ │ │ ├── LogPanel.tsx # 日志面板 │ │ │ └── NodeStatus.tsx # 节点状态 │ │ │ │ │ └── ApprovalCenter/ # 审批中心 │ │ ├── index.tsx │ │ ├── TaskList.tsx # 任务列表 │ │ └── ApprovalForm.tsx # 审批表单 │ │ │ ├── pages/ # 页面 │ │ ├── WorkflowListPage.tsx # 工作流列表 │ │ ├── WorkflowEditorPage.tsx # 工作流编辑 │ │ ├── ExecutionHistoryPage.tsx # 执行历史 │ │ └── ApprovalCenterPage.tsx # 审批中心 │ │ │ ├── store/ # 状态管理(Zustand) │ │ ├── workflowStore.ts # 工作流状态 │ │ ├── nodeTypeStore.ts # 节点类型 │ │ └── executionStore.ts # 执行状态 │ │ │ ├── types/ # TypeScript类型定义 │ │ ├── workflow.ts │ │ ├── node.ts │ │ └── execution.ts │ │ │ ├── utils/ # 工具函数 │ │ ├── expressionParser.ts # 表达式解析 │ │ ├── schemaBuilder.ts # Schema构建 │ │ └── validation.ts # 表单验证 │ │ │ ├── App.tsx │ ├── main.tsx │ └── index.css │ ├── package.json ├── tsconfig.json ├── vite.config.ts └── README.md ``` --- ## 二、核心组件实现 ### 2.1 工作流编辑器(WorkflowEditor) #### Canvas.tsx - ReactFlow 画布 ```tsx import React, { useCallback, useRef } from 'react'; import ReactFlow, { Node, Edge, Controls, Background, MiniMap, useNodesState, useEdgesState, addEdge, Connection, NodeTypes, BackgroundVariant, } from 'reactflow'; import 'reactflow/dist/style.css'; import CustomNode from './CustomNode'; import { useWorkflowStore } from '@/store/workflowStore'; // 自定义节点类型 const nodeTypes: NodeTypes = { custom: CustomNode, }; interface Props { onNodeClick: (node: Node) => void; } export default function Canvas({ onNodeClick }: Props) { const reactFlowWrapper = useRef(null); // 从store获取状态 const { nodes, edges, setNodes, setEdges } = useWorkflowStore(); // ReactFlow hooks const [rfNodes, setRfNodes, onNodesChange] = useNodesState(nodes); const [rfEdges, setRfEdges, onEdgesChange] = useEdgesState(edges); /** * 连线处理(重要)⭐ * * 验证规则: * 1. 不能连接到自己 * 2. 两个节点间只能有一条连线 * 3. 不能形成环(可选) */ const onConnect = useCallback( (connection: Connection) => { // 验证:不能连接到自己 if (connection.source === connection.target) { message.warning('节点不能连接到自己'); return; } // 验证:检查是否已存在连线 const existingEdge = rfEdges.find( (edge) => edge.source === connection.source && edge.target === connection.target ); if (existingEdge) { message.warning('这两个节点已经连接'); return; } // 添加连线 const newEdge = { ...connection, id: `edge-${connection.source}-${connection.target}`, type: 'smoothstep', animated: true, }; setRfEdges((eds) => addEdge(newEdge, eds)); setEdges(addEdge(newEdge, edges)); }, [rfEdges, edges, setEdges, setRfEdges] ); /** * 节点拖拽结束(更新位置) */ const onNodeDragStop = useCallback( (event: React.MouseEvent, node: Node) => { setNodes( nodes.map((n) => n.id === node.id ? { ...n, position: node.position } : n ) ); }, [nodes, setNodes] ); /** * 从节点面板拖拽到画布(重要)⭐⭐ */ const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect(); const nodeTypeData = event.dataTransfer.getData('application/reactflow'); if (!nodeTypeData || !reactFlowBounds) { return; } const nodeType = JSON.parse(nodeTypeData); // 计算放置位置 const position = { x: event.clientX - reactFlowBounds.left, y: event.clientY - reactFlowBounds.top, }; // 创建新节点 const newNode: Node = { id: `node-${Date.now()}`, type: 'custom', position, data: { type: nodeType.id, name: nodeType.displayName, icon: nodeType.icon, config: {}, // 空配置,等待用户填写 }, }; setRfNodes((nds) => nds.concat(newNode)); setNodes([...nodes, newNode]); }, [nodes, setNodes, setRfNodes] ); const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; }, []); return (
onNodeClick(node)} onNodeDragStop={onNodeDragStop} fitView snapToGrid snapGrid={[15, 15]} >
); } ``` #### CustomNode.tsx - 自定义节点样式 ```tsx import React, { memo } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; import { Card, Tag, Typography } from 'antd'; import * as Icons from '@ant-design/icons'; const { Text } = Typography; /** * 自定义节点组件 * * 功能: * 1. 显示节点图标和名称 * 2. 显示节点状态(执行中、成功、失败) * 3. 连接点(上下左右) */ function CustomNode({ data, selected }: NodeProps) { // 动态获取图标 const IconComponent = (Icons as any)[data.icon] || Icons.ApiOutlined; // 节点状态颜色 const getStatusColor = () => { switch (data.status) { case 'running': return '#1890ff'; case 'success': return '#52c41a'; case 'failed': return '#ff4d4f'; default: return '#d9d9d9'; } }; return (
{/* 连接点 - 上 */} {/* 节点内容 */}
{data.name}
{data.type} {/* 状态标签 */} {data.status && (
{data.status === 'running' && '执行中'} {data.status === 'success' && '成功'} {data.status === 'failed' && '失败'}
)}
{/* 连接点 - 下 */}
); } export default memo(CustomNode); ``` #### NodePalette.tsx - 节点面板 ```tsx import React, { useEffect } from 'react'; import { Card, Collapse, Space, Typography, Empty, Spin } from 'antd'; import * as Icons from '@ant-design/icons'; import { useNodeTypeStore } from '@/store/nodeTypeStore'; const { Panel } = Collapse; const { Text } = Typography; /** * 节点面板 * * 功能: * 1. 显示所有可用节点类型 * 2. 按分类分组 * 3. 支持拖拽到画布 */ export default function NodePalette() { const { nodeTypes, loading, fetchNodeTypes } = useNodeTypeStore(); useEffect(() => { fetchNodeTypes(); }, [fetchNodeTypes]); /** * 开始拖拽(重要)⭐ */ const onDragStart = (event: React.DragEvent, nodeType: NodeTypeMetadata) => { event.dataTransfer.setData( 'application/reactflow', JSON.stringify(nodeType) ); event.dataTransfer.effectAllowed = 'move'; }; // 按分类分组 const groupedNodeTypes = nodeTypes.reduce((acc, nodeType) => { const category = nodeType.category || 'other'; if (!acc[category]) { acc[category] = []; } acc[category].push(nodeType); return acc; }, {} as Record); // 分类显示名称 const categoryNames: Record = { api: 'API', database: '数据库', logic: '逻辑控制', notification: '通知', transform: '数据转换', other: '其他', }; if (loading) { return (
); } return (
节点库 {Object.keys(groupedNodeTypes).length === 0 ? ( ) : ( {Object.entries(groupedNodeTypes).map(([category, nodes]) => ( {nodes.map((nodeType) => { const IconComponent = (Icons as any)[nodeType.icon] || Icons.ApiOutlined; return ( onDragStart(e, nodeType)} style={{ cursor: 'grab', borderRadius: 8, }} >
{nodeType.displayName}
{nodeType.description}
); })}
))}
)}
); } ``` ### 2.2 节点配置面板(最重要)⭐⭐⭐ #### index.tsx - 主组件 ```tsx import React, { useMemo } from 'react'; import { Form, Input, Select, Button, Space, Typography, Divider } from 'antd'; import { Node, Edge } from 'reactflow'; import { NodeTypeMetadata, FieldDefinition } from '@/types/node'; import FieldMappingSelector from './FieldMappingSelector'; import ExpressionInput from './ExpressionInput'; import CodeEditor from './CodeEditor'; const { Title, Text } = Typography; interface Props { node: Node | null; nodes: Node[]; edges: Edge[]; nodeTypes: Record; onConfigChange: (nodeId: string, config: any) => void; onClose: () => void; } /** * 节点配置面板 * * 核心职责: * 1. 根据节点类型动态生成表单 * 2. 提供字段映射选择器(从上游节点选择输出字段)⭐⭐⭐ * 3. 支持表达式输入 * 4. 实时保存配置 */ export default function NodeConfigPanel({ node, nodes, edges, nodeTypes, onConfigChange, onClose, }: Props) { const [form] = Form.useForm(); if (!node) { return (
请选择一个节点
); } // 获取节点类型元数据 const nodeMetadata = nodeTypes[node.data.type]; if (!nodeMetadata) { return
节点类型未找到: {node.data.type}
; } // 计算上游节点(重要)⭐⭐ const upstreamNodes = useMemo(() => { // 找到所有指向当前节点的边 const incomingEdges = edges.filter((edge) => edge.target === node.id); // 获取上游节点的详细信息 return incomingEdges .map((edge) => nodes.find((n) => n.id === edge.source)) .filter((n): n is Node => n !== undefined); }, [node.id, nodes, edges]); /** * 表单值变化时保存 */ const handleValuesChange = (changedValues: any, allValues: any) => { onConfigChange(node.id, allValues); }; /** * 根据字段类型渲染输入组件(关键)⭐⭐⭐ */ const renderFieldInput = (field: FieldDefinition) => { // 1. 字段映射选择器(最核心)⭐⭐⭐ if (field.supportsFieldMapping) { return ( ); } // 2. 表达式输入框 if (field.supportsExpression) { return ( ); } // 3. 代码编辑器 if (field.type === 'code') { return ( ); } // 4. 下拉选择 if (field.type === 'select') { return ( ); } // 5. 数字输入 if (field.type === 'number') { return ( ); } // 6. 多行文本 if (field.type === 'textarea') { return ( ); } // 7. 默认:单行文本 return ( ); }; return (
{/* 标题 */}
{node.data.name} {nodeMetadata.description}
{/* 动态表单 */}
{nodeMetadata.fields.map((field) => ( {field.label} {field.required && *} {field.supportsExpression && ( (支持表达式) )} } rules={[ { required: field.required, message: `请输入${field.label}`, }, ]} tooltip={field.description} > {renderFieldInput(field)} ))}
{/* 上游节点信息 */} {upstreamNodes.length > 0 && (
可引用的上游节点:
{upstreamNodes.map((upstream) => ( {upstream.data.name} ))}
)} {/* 底部操作 */}
); } ``` #### FieldMappingSelector.tsx - 字段映射选择器(核心)⭐⭐⭐ ```tsx import React, { useState, useMemo } from 'react'; import { TreeSelect, Input, Space, Button, Tag, Tooltip } from 'antd'; import { FunctionOutlined, EditOutlined } from '@ant-design/icons'; import { Node } from 'reactflow'; import { NodeTypeMetadata, FieldDefinition } from '@/types/node'; interface Props { field: FieldDefinition; upstreamNodes: Node[]; nodeTypes: Record; value?: string; onChange?: (value: string) => void; placeholder?: string; } /** * 字段映射选择器(最核心的组件)⭐⭐⭐ * * 功能: * 1. 显示上游节点的输出字段树形结构 * 2. 用户可以选择字段,自动生成表达式 * 3. 也可以手动输入表达式 * * 示例: * 用户选择:nodes.httpRequest.output.body.email * 生成表达式:${nodes.httpRequest.output.body.email} */ export default function FieldMappingSelector({ field, upstreamNodes, nodeTypes, value, onChange, placeholder, }: Props) { const [mode, setMode] = useState<'select' | 'expression'>('select'); /** * 构建字段树(核心逻辑)⭐⭐⭐ * * 根据上游节点的outputSchema构建树形选择结构 */ const fieldTree = useMemo(() => { return upstreamNodes.map((node) => { const nodeType = nodeTypes[node.data.type]; if (!nodeType?.outputSchema) { return null; } return { title: ( {node.data.name || node.id} {nodeType.displayName} ), value: `nodes.${node.id}`, selectable: false, children: buildFieldTree( nodeType.outputSchema.properties || {}, `nodes.${node.id}.output`, 0 ), }; }).filter(Boolean); }, [upstreamNodes, nodeTypes]); // 选择模式 if (mode === 'select') { return ( onChange?.(`\${${val}}`)} // 添加 ${} treeData={fieldTree} placeholder={placeholder || '选择上游节点的字段'} showSearch treeDefaultExpandAll dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} treeLine={{ showLeafIcon: false }} filterTreeNode={(input, treeNode) => { return (treeNode.title as string) .toLowerCase() .includes(input.toLowerCase()); }} />