- Run cargo fmt on all Rust crates for consistent formatting - Update CLAUDE.md with WASM plugin commands and dev.ps1 instructions - Update wiki: add WASM plugin architecture, rewrite dev environment docs - Minor frontend cleanup (unused imports)
273 lines
8.9 KiB
TypeScript
273 lines
8.9 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { Button, Form, Input, message, Spin } from 'antd';
|
|
import {
|
|
ReactFlow,
|
|
Controls,
|
|
Background,
|
|
addEdge,
|
|
useNodesState,
|
|
useEdgesState,
|
|
type Connection,
|
|
type Node,
|
|
type Edge,
|
|
BackgroundVariant,
|
|
MarkerType,
|
|
} from '@xyflow/react';
|
|
import '@xyflow/react/dist/style.css';
|
|
import {
|
|
type CreateProcessDefinitionRequest,
|
|
type NodeDef,
|
|
type EdgeDef,
|
|
getProcessDefinition,
|
|
} from '../../api/workflowDefinitions';
|
|
|
|
const NODE_TYPES_MAP: Record<string, { label: string; color: string }> = {
|
|
StartEvent: { label: '开始', color: '#52c41a' },
|
|
EndEvent: { label: '结束', color: '#ff4d4f' },
|
|
UserTask: { label: '用户任务', color: '#1890ff' },
|
|
ServiceTask: { label: '服务任务', color: '#722ed1' },
|
|
ExclusiveGateway: { label: '排他网关', color: '#fa8c16' },
|
|
ParallelGateway: { label: '并行网关', color: '#13c2c2' },
|
|
};
|
|
|
|
const PALETTE_ITEMS = Object.entries(NODE_TYPES_MAP).map(([type, info]) => ({
|
|
type,
|
|
label: info.label,
|
|
color: info.color,
|
|
}));
|
|
|
|
function createFlowNode(type: string, label: string, position: { x: number; y: number }, id?: string): Node {
|
|
return {
|
|
id: id || `node_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
type: 'default',
|
|
position,
|
|
data: { label: `${label}`, nodeType: type, name: label },
|
|
style: {
|
|
background: NODE_TYPES_MAP[type]?.color || '#f0f0f0',
|
|
color: '#fff',
|
|
padding: '8px 16px',
|
|
borderRadius: type.includes('Gateway') ? 0 : type === 'StartEvent' || type === 'EndEvent' ? 50 : 6,
|
|
fontSize: 13,
|
|
fontWeight: 500,
|
|
border: '2px solid rgba(255,255,255,0.3)',
|
|
width: type.includes('Gateway') ? 80 : 140,
|
|
textAlign: 'center' as const,
|
|
},
|
|
};
|
|
}
|
|
|
|
interface ProcessDesignerProps {
|
|
definitionId: string | null;
|
|
onSave: (req: CreateProcessDefinitionRequest, id?: string) => void;
|
|
}
|
|
|
|
export default function ProcessDesigner({ definitionId, onSave }: ProcessDesignerProps) {
|
|
const [form] = Form.useForm();
|
|
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
|
|
|
const isEditing = definitionId !== null;
|
|
|
|
// 加载流程定义(编辑模式)或初始化默认节点(新建模式)
|
|
useEffect(() => {
|
|
if (!definitionId) {
|
|
const startNode = createFlowNode('StartEvent', '开始', { x: 250, y: 50 });
|
|
const userNode = createFlowNode('UserTask', '审批', { x: 250, y: 200 });
|
|
const endNode = createFlowNode('EndEvent', '结束', { x: 250, y: 400 });
|
|
setNodes([startNode, userNode, endNode]);
|
|
setEdges([
|
|
{ id: 'e_start_approve', source: startNode.id, target: userNode.id, markerEnd: { type: MarkerType.ArrowClosed } },
|
|
{ id: 'e_approve_end', source: userNode.id, target: endNode.id, markerEnd: { type: MarkerType.ArrowClosed } },
|
|
]);
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
getProcessDefinition(definitionId)
|
|
.then((def) => {
|
|
form.setFieldsValue({
|
|
name: def.name,
|
|
key: def.key,
|
|
category: def.category,
|
|
description: def.description,
|
|
});
|
|
const flowNodes = def.nodes.map((n, i) =>
|
|
createFlowNode(n.type, n.name, n.position || { x: 200, y: i * 120 + 50 }, n.id)
|
|
);
|
|
const flowEdges: Edge[] = def.edges.map((e) => ({
|
|
id: e.id,
|
|
source: e.source,
|
|
target: e.target,
|
|
markerEnd: { type: MarkerType.ArrowClosed },
|
|
label: e.label || e.condition,
|
|
}));
|
|
setNodes(flowNodes);
|
|
setEdges(flowEdges);
|
|
})
|
|
.catch(() => message.error('加载流程定义失败'))
|
|
.finally(() => setLoading(false));
|
|
}, [definitionId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const onConnect = useCallback(
|
|
(connection: Connection) => {
|
|
setEdges((eds) =>
|
|
addEdge(
|
|
{ ...connection, markerEnd: { type: MarkerType.ArrowClosed } },
|
|
eds,
|
|
),
|
|
);
|
|
},
|
|
[setEdges],
|
|
);
|
|
|
|
const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
|
|
setSelectedNode(node);
|
|
}, []);
|
|
|
|
const handleAddNode = (type: string) => {
|
|
const info = NODE_TYPES_MAP[type];
|
|
if (!info) return;
|
|
const newNode = createFlowNode(type, info.label, {
|
|
x: 100 + Math.random() * 400,
|
|
y: 100 + Math.random() * 300,
|
|
});
|
|
setNodes((nds) => [...nds, newNode]);
|
|
};
|
|
|
|
const handleDeleteNode = () => {
|
|
if (!selectedNode) return;
|
|
setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id));
|
|
setEdges((eds) =>
|
|
eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id),
|
|
);
|
|
setSelectedNode(null);
|
|
};
|
|
|
|
const handleUpdateNodeName = (name: string) => {
|
|
if (!selectedNode) return;
|
|
setNodes((nds) =>
|
|
nds.map((n) =>
|
|
n.id === selectedNode.id
|
|
? { ...n, data: { ...n.data, label: name, name } }
|
|
: n,
|
|
),
|
|
);
|
|
setSelectedNode((prev) => (prev ? { ...prev, data: { ...prev.data, label: name, name } } : null));
|
|
};
|
|
|
|
const handleSave = () => {
|
|
form.validateFields().then((values) => {
|
|
const flowNodes: NodeDef[] = nodes.map((n) => ({
|
|
id: n.id,
|
|
type: (n.data.nodeType as NodeDef['type']) || 'UserTask',
|
|
name: String(n.data.name || n.data.label || ''),
|
|
position: { x: Math.round(n.position.x), y: Math.round(n.position.y) },
|
|
}));
|
|
const flowEdges: EdgeDef[] = edges.map((e) => ({
|
|
id: e.id,
|
|
source: e.source,
|
|
target: e.target,
|
|
label: e.label ? String(e.label) : undefined,
|
|
}));
|
|
onSave(
|
|
{ ...values, nodes: flowNodes, edges: flowEdges },
|
|
definitionId || undefined,
|
|
);
|
|
}).catch(() => {
|
|
message.error('请填写必要字段');
|
|
});
|
|
};
|
|
|
|
const defaultEdgeOptions = useMemo(
|
|
() => ({
|
|
markerEnd: { type: MarkerType.ArrowClosed },
|
|
}),
|
|
[],
|
|
);
|
|
|
|
if (loading) {
|
|
return <div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}><Spin /></div>;
|
|
}
|
|
|
|
return (
|
|
<div style={{ display: 'flex', gap: 16, height: 500 }}>
|
|
{/* 左侧工具面板 */}
|
|
<div style={{ width: 180, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
<p style={{ fontWeight: 500, margin: '0 0 4px' }}>添加节点</p>
|
|
{PALETTE_ITEMS.map((item) => (
|
|
<Button
|
|
key={item.type}
|
|
size="small"
|
|
style={{ textAlign: 'left' }}
|
|
onClick={() => handleAddNode(item.type)}
|
|
>
|
|
<span style={{
|
|
display: 'inline-block',
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: 2,
|
|
background: item.color,
|
|
marginRight: 6,
|
|
}} />
|
|
{item.label}
|
|
</Button>
|
|
))}
|
|
|
|
{selectedNode && (
|
|
<div style={{ marginTop: 16, padding: 8, background: '#f5f5f5', borderRadius: 6 }}>
|
|
<p style={{ fontWeight: 500, margin: '0 0 8px', fontSize: 12 }}>节点属性</p>
|
|
<Input
|
|
size="small"
|
|
value={String(selectedNode.data.name || '')}
|
|
onChange={(e) => handleUpdateNodeName(e.target.value)}
|
|
placeholder="节点名称"
|
|
style={{ marginBottom: 8 }}
|
|
/>
|
|
<Button size="small" danger onClick={handleDeleteNode} block>
|
|
删除节点
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 中间画布 */}
|
|
<div style={{ flex: 1, border: '1px solid #d9d9d9', borderRadius: 6 }}>
|
|
<ReactFlow
|
|
nodes={nodes}
|
|
edges={edges}
|
|
onNodesChange={onNodesChange}
|
|
onEdgesChange={onEdgesChange}
|
|
onConnect={onConnect}
|
|
onNodeClick={onNodeClick}
|
|
defaultEdgeOptions={defaultEdgeOptions}
|
|
fitView
|
|
>
|
|
<Controls />
|
|
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
|
|
</ReactFlow>
|
|
</div>
|
|
|
|
{/* 右侧表单 */}
|
|
<div style={{ width: 220, overflow: 'auto' }}>
|
|
<Form form={form} layout="vertical" size="small">
|
|
<Form.Item name="name" label="流程名称" rules={[{ required: true, message: '请输入' }]}>
|
|
<Input placeholder="请假审批" />
|
|
</Form.Item>
|
|
<Form.Item name="key" label="流程编码" rules={[{ required: true, message: '请输入' }]}>
|
|
<Input placeholder="leave_approval" disabled={isEditing} />
|
|
</Form.Item>
|
|
<Form.Item name="category" label="分类">
|
|
<Input placeholder="leave" />
|
|
</Form.Item>
|
|
<Form.Item name="description" label="描述">
|
|
<Input.TextArea rows={2} />
|
|
</Form.Item>
|
|
<Button type="primary" onClick={handleSave}>{isEditing ? '更新' : '保存'}</Button>
|
|
</Form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|