workflow底层UI库整合至项目,优化构建逻辑
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Background,
|
||||
Controls,
|
||||
type Edge,
|
||||
type Handle,
|
||||
MarkerType,
|
||||
MiniMap,
|
||||
type Node,
|
||||
type NodeTypes,
|
||||
Panel,
|
||||
SvelteFlow,
|
||||
useSvelteFlow
|
||||
} from '@xyflow/svelte';
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
import '../styles/index.ts';
|
||||
import {store} from '#store/stores.svelte';
|
||||
import {nodeTypes} from './nodes';
|
||||
import Toolbar from './Toolbar.svelte';
|
||||
import {genShortId} from './utils/IdGen';
|
||||
import {useGetNode} from './utils/useGetNode.svelte';
|
||||
import {useEnsureParentInNodesBefore} from './utils/useEnsureParentInNodesBefore.svelte';
|
||||
import {Textarea} from './base';
|
||||
import {useGetEdgesByTarget} from './utils/useGetEdgesByTarget.svelte';
|
||||
import {getOptions} from './utils/NodeUtils';
|
||||
import CustomNode from './nodes/CustomNode.svelte';
|
||||
import {useUpdateEdgeData} from './utils/useUpdateEdgeData.svelte';
|
||||
import {Button} from '#components/base/index';
|
||||
import {useDeleteEdge} from '#components/utils/useDeleteEdge.svelte';
|
||||
import {useGetNodesFromSource} from '#components/utils/useGetNodesFromSource.svelte';
|
||||
import {useGetNodeRelativePosition} from '#components/utils/useGetNodeRelativePosition.svelte';
|
||||
import {useCopyPasteHandler} from '#components/utils/useCopyPasteHandler.svelte';
|
||||
import {onDestroy, onMount} from 'svelte';
|
||||
import {isInEditableElement} from '#components/utils/isInEditableElement';
|
||||
|
||||
const { onInit, ...rest } = $props();
|
||||
const svelteFlow = useSvelteFlow();
|
||||
|
||||
console.log('props', rest);
|
||||
onInit(svelteFlow);
|
||||
|
||||
let showEdgePanel = $state(false);
|
||||
let currentEdge = $state<Edge | null>(null);
|
||||
|
||||
const { updateEdgeData } = useUpdateEdgeData();
|
||||
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const position = svelteFlow.screenToFlowPosition({
|
||||
x: event.clientX - 250,
|
||||
y: event.clientY - 100
|
||||
});
|
||||
|
||||
const baseNodeJsonString = event.dataTransfer?.getData('application/tinyflow');
|
||||
if (!baseNodeJsonString) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseNode = JSON.parse(baseNodeJsonString);
|
||||
const newNode = {
|
||||
id: `node_${genShortId()}`,
|
||||
position,
|
||||
data: {},
|
||||
...baseNode
|
||||
} satisfies Node;
|
||||
|
||||
store.addNode(newNode);
|
||||
store.selectNodeOnly(newNode.id);
|
||||
};
|
||||
|
||||
|
||||
const { getNode } = useGetNode();
|
||||
|
||||
|
||||
const isValidConnection = (conn: any) => {
|
||||
const sourceNode = getNode(conn.source)!;
|
||||
const targetNode = getNode(conn.target)!;
|
||||
|
||||
// 阻止循环节点连接到父级节点 或者 父级节点连接到子级节点
|
||||
if (conn.sourceHandle === 'loop_handle' || sourceNode.parentId) {
|
||||
const edges = svelteFlow.getEdges();
|
||||
for (let edge of edges) {
|
||||
if (edge.target === conn.target) {
|
||||
const edgeSourceNode = getNode(edge.source) as Node;
|
||||
if (conn.sourceHandle === 'loop_handle' && edgeSourceNode.parentId !== sourceNode.id) {
|
||||
return false;
|
||||
}
|
||||
if (sourceNode.parentId && edgeSourceNode.parentId !== sourceNode.parentId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceNode.parentId && targetNode.parentId && targetNode.parentId !== sourceNode.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 允许链接
|
||||
return true;
|
||||
};
|
||||
|
||||
const { getNodesFromSource } = useGetNodesFromSource();
|
||||
const { getNodeRelativePosition } = useGetNodeRelativePosition();
|
||||
const { ensureParentInNodesBefore } = useEnsureParentInNodesBefore();
|
||||
const onconnectend = (_: any, state: any) => {
|
||||
if (!state.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toNode = state.toNode as Node;
|
||||
if (toNode.parentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromNode = state.fromNode as Node;
|
||||
const fromHande = state.fromHandle as Handle;
|
||||
|
||||
const newNode = {
|
||||
position: { ...toNode.position }
|
||||
} as Node;
|
||||
|
||||
if (fromHande.id === 'loop_handle') {
|
||||
newNode.parentId = fromNode.id;
|
||||
} else if (fromNode.parentId) {
|
||||
newNode.parentId = fromNode.parentId;
|
||||
}
|
||||
|
||||
if (newNode.parentId) {
|
||||
|
||||
const { x, y } = getNodeRelativePosition(newNode.parentId);
|
||||
|
||||
newNode.position = {
|
||||
x: toNode.position.x - x,
|
||||
y: toNode.position.y - y
|
||||
};
|
||||
|
||||
svelteFlow.updateNode(toNode.id, newNode);
|
||||
|
||||
// 更新目标节点的所有后续的链接节点
|
||||
const nodesFromToNode = getNodesFromSource(toNode.id);
|
||||
nodesFromToNode.forEach((node) => {
|
||||
svelteFlow.updateNode(node.id, {
|
||||
parentId: newNode.parentId,
|
||||
position: {
|
||||
x: node.position.x - x,
|
||||
y: node.position.y - y
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ensureParentInNodesBefore(newNode.parentId, toNode.id);
|
||||
}
|
||||
|
||||
// 显示边面板
|
||||
setTimeout(() => {
|
||||
store.getEdges().forEach((edge) => {
|
||||
if (edge.target === toNode.id && edge.source == fromNode.id) {
|
||||
showEdgePanel = true;
|
||||
currentEdge = edge;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const { getEdgesByTarget } = useGetEdgesByTarget();
|
||||
const onDelete = (params: any) => {
|
||||
const deleteEdges = params.edges as Edge[];
|
||||
deleteEdges.forEach((edge) => {
|
||||
if (edge.id === currentEdge?.id) {
|
||||
currentEdge = null;
|
||||
showEdgePanel = false;
|
||||
}
|
||||
const targetNode = getNode(edge.target) as Node;
|
||||
if (targetNode && targetNode.parentId) {
|
||||
const nodeEdges = getEdgesByTarget(edge.target);
|
||||
// const loopNode = getNode(targetNode.parentId) as Node;
|
||||
const { x, y } = getNodeRelativePosition(targetNode.parentId);
|
||||
if (nodeEdges.length === 0) {
|
||||
svelteFlow.updateNode(targetNode.id, {
|
||||
parentId: undefined,
|
||||
position: {
|
||||
x: targetNode.position.x + x,
|
||||
y: targetNode.position.y + y
|
||||
}
|
||||
});
|
||||
|
||||
// 更新目标节点的所有后续的链接节点
|
||||
const nodesFromSource = getNodesFromSource(targetNode.id);
|
||||
nodesFromSource.forEach((node) => {
|
||||
svelteFlow.updateNode(node.id, {
|
||||
parentId: undefined,
|
||||
position: {
|
||||
x: node.position.x + x,
|
||||
y: node.position.y + y
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
let hasSameParent = false;
|
||||
for (let i = 0; i < nodeEdges.length; i++) {
|
||||
const edge = nodeEdges[i];
|
||||
const sourceNode = getNode(edge.source) as Node;
|
||||
if (sourceNode.parentId || sourceNode.type === 'loopNode') {
|
||||
hasSameParent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasSameParent) {
|
||||
svelteFlow.updateNode(targetNode.id, {
|
||||
parentId: undefined,
|
||||
position: {
|
||||
x: targetNode.position.x + x,
|
||||
y: targetNode.position.y + y
|
||||
}
|
||||
});
|
||||
|
||||
// 更新目标节点的所有后续的链接节点
|
||||
const nodesFromSource = getNodesFromSource(targetNode.id);
|
||||
nodesFromSource.forEach((node) => {
|
||||
svelteFlow.updateNode(node.id, {
|
||||
parentId: undefined,
|
||||
position: {
|
||||
x: node.position.x + x,
|
||||
y: node.position.y + y
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const { deleteEdge } = useDeleteEdge();
|
||||
|
||||
|
||||
const onconnectstart = (event: any, node: any) => {
|
||||
// console.log('onconnectstart: ', event, node);
|
||||
};
|
||||
|
||||
|
||||
const onconnect = (event: any) => {
|
||||
// console.log('onconnect: ', event);
|
||||
};
|
||||
|
||||
const { copyHandler, pasteHandler } = useCopyPasteHandler();
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
||||
if (isInEditableElement()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
|
||||
e.preventDefault();
|
||||
copyHandler(e);
|
||||
}
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
||||
e.preventDefault();
|
||||
// 全选所有节点
|
||||
store.updateNodes((nodes) => {
|
||||
return nodes.map(node => ({ ...node, selected: true }));
|
||||
});
|
||||
|
||||
store.updateEdges((edges) => {
|
||||
return edges.map(edge => ({ ...edge, selected: true }));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleGlobalPaste = async (event: ClipboardEvent) => {
|
||||
// 只在“非输入态”下处理流程图粘贴
|
||||
if (isInEditableElement()) {
|
||||
return;
|
||||
}
|
||||
|
||||
pasteHandler(event);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('paste', handleGlobalPaste);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('paste', handleGlobalPaste);
|
||||
});
|
||||
|
||||
const customNodeTypes = {
|
||||
// ...nodeTypes
|
||||
} as NodeTypes;
|
||||
|
||||
const customNodes = getOptions().customNodes;
|
||||
if (customNodes) {
|
||||
for (let key of Object.keys(customNodes)) {
|
||||
customNodeTypes[key] = CustomNode as any;
|
||||
}
|
||||
}
|
||||
|
||||
const onDataChange = getOptions().onDataChange;
|
||||
$effect(() => {
|
||||
onDataChange?.({
|
||||
nodes: store.getNodes(),
|
||||
edges: store.getEdges(),
|
||||
viewport: store.getViewport()
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div style="position: relative; height: 100%; width: 100%;overflow: hidden">
|
||||
<SvelteFlow nodeTypes={{ ...nodeTypes, ...customNodeTypes}}
|
||||
bind:nodes={store.getNodes, store.setNodes}
|
||||
bind:edges={store.getEdges, store.setEdges}
|
||||
bind:viewport={store.getViewport, store.setViewport}
|
||||
ondrop={onDrop}
|
||||
ondragover={onDragOver}
|
||||
isValidConnection={isValidConnection}
|
||||
onconnectend={onconnectend}
|
||||
onconnectstart={onconnectstart}
|
||||
onconnect={onconnect}
|
||||
connectionRadius={50}
|
||||
onedgeclick={(e) => {
|
||||
showEdgePanel = true;
|
||||
currentEdge = e.edge;
|
||||
}}
|
||||
onbeforeconnect={(edge) => {
|
||||
return {
|
||||
...edge,
|
||||
id:genShortId(),
|
||||
}
|
||||
}}
|
||||
ondelete={onDelete}
|
||||
onclick={(e) => {
|
||||
const el = e.target as HTMLElement;
|
||||
if (el.classList.contains("svelte-flow__edge-interaction")
|
||||
|| el.classList.contains('panel-content')
|
||||
|| el.closest('.panel-content')){
|
||||
return
|
||||
}
|
||||
showEdgePanel = false;
|
||||
currentEdge = null;
|
||||
}}
|
||||
defaultEdgeOptions={{
|
||||
// animated: true,
|
||||
// label: 'edge label',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
// color: 'red',
|
||||
width: 20,
|
||||
height: 20
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
<MiniMap />
|
||||
|
||||
{#if showEdgePanel}
|
||||
<Panel>
|
||||
<div class="panel-content">
|
||||
<div>边属性设置</div>
|
||||
<div class="setting-title">边条件设置</div>
|
||||
<div class="setting-item">
|
||||
<Textarea
|
||||
rows={3}
|
||||
placeholder="请输入边条件"
|
||||
style="width: 100%"
|
||||
value={currentEdge?.data?.condition}
|
||||
onchange={(e)=>{
|
||||
if (currentEdge){
|
||||
updateEdgeData(currentEdge.id, {
|
||||
condition: e.target?.value
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="setting-item" style="padding: 8px 0">
|
||||
<Button
|
||||
onclick={() => {
|
||||
deleteEdge(currentEdge?.id)
|
||||
showEdgePanel = false;
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
primary={true}
|
||||
onclick={() => {
|
||||
showEdgePanel = false;
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
{/if}
|
||||
</SvelteFlow>
|
||||
<Toolbar />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel-content {
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
width: 200px;
|
||||
border: 1px solid #efefef;
|
||||
}
|
||||
|
||||
.setting-title {
|
||||
margin: 10px 0;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user