SvelteFlow自定义节点Skill svelteflow-custom-nodes

这个技能专注于使用 Svelte Flow 库创建自定义节点和边,实现复杂的前端图形界面编辑器。它涵盖自定义节点组件、可调整大小节点、工具栏和交互式功能,适用于构建节点编辑器和工作流界面。关键词:Svelte Flow、自定义节点、前端开发、图形界面、节点编辑器、Svelte 组件。

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: svelteflow-custom-nodes 用户可调用: false 描述: 用于创建自定义 Svelte Flow 节点、边和句柄。涵盖自定义节点组件、可调整大小的节点、工具栏和高级自定义。 允许工具:

  • Bash
  • Read

Svelte Flow 自定义节点和边

使用 Svelte Flow 创建完全自定义的节点和边。构建具有自定义样式、行为和交互的复杂节点编辑器。

自定义节点组件

<!-- TextUpdaterNode.svelte -->
<script lang="ts">
  import { Handle, Position } from '@xyflow/svelte';

  export let id: string;
  export let data: { label: string };
  export let isConnectable: boolean;

  function handleChange(event: Event) {
    const target = event.target as HTMLInputElement;
    // 分派自定义事件或更新存储
    console.log('值已更改:', target.value);
  }
</script>

<div class="text-updater-node">
  <Handle type="target" position={Position.Top} {isConnectable} />

  <div class="content">
    <label for="text">文本:</label>
    <input
      id="text"
      name="text"
      on:input={handleChange}
      class="nodrag"
      value={data.label}
    />
  </div>

  <Handle type="source" position={Position.Bottom} id="a" {isConnectable} />
</div>

<style>
  .text-updater-node {
    background: white;
    border: 1px solid #1a192b;
    border-radius: 8px;
    padding: 10px;
  }

  .content {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }

  input {
    padding: 4px 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }
</style>

注册自定义节点

<!-- Flow.svelte -->
<script lang="ts">
  import { SvelteFlow, type Node, type NodeTypes } from '@xyflow/svelte';
  import { writable } from 'svelte/store';
  import TextUpdaterNode from './TextUpdaterNode.svelte';
  import ColorPickerNode from './ColorPickerNode.svelte';

  // 定义节点类型
  const nodeTypes: NodeTypes = {
    textUpdater: TextUpdaterNode,
    colorPicker: ColorPickerNode,
  };

  const nodes = writable<Node[]>([
    {
      id: '1',
      type: 'textUpdater',
      position: { x: 0, y: 0 },
      data: { label: 'Hello' },
    },
    {
      id: '2',
      type: 'colorPicker',
      position: { x: 200, y: 100 },
      data: { color: '#ff0000' },
    },
  ]);

  const edges = writable([]);
</script>

<SvelteFlow {nodes} {edges} {nodeTypes} fitView />

使用 Tailwind 的样式节点

<!-- StatusNode.svelte -->
<script lang="ts">
  import { Handle, Position } from '@xyflow/svelte';

  export let data: {
    label: string;
    status: 'pending' | 'running' | 'completed' | 'error';
  };

  const statusConfig = {
    pending: { bg: 'bg-yellow-100', border: 'border-yellow-400', icon: '⏳' },
    running: { bg: 'bg-blue-100', border: 'border-blue-400', icon: '⚡' },
    completed: { bg: 'bg-green-100', border: 'border-green-400', icon: '✅' },
    error: { bg: 'bg-red-100', border: 'border-red-400', icon: '❌' },
  };

  $: config = statusConfig[data.status];
</script>

<div class="px-4 py-2 rounded-lg border-2 shadow-sm {config.bg} {config.border}">
  <Handle type="target" position={Position.Top} class="!bg-gray-400" />

  <div class="flex items-center gap-2">
    <span class="text-xl">{config.icon}</span>
    <span class="font-medium">{data.label}</span>
  </div>

  <Handle type="source" position={Position.Bottom} class="!bg-gray-400" />
</div>

带多个句柄的节点

<!-- SwitchNode.svelte -->
<script lang="ts">
  import { Handle, Position } from '@xyflow/svelte';

  export let data: {
    label: string;
    cases: string[];
  };
</script>

<div class="switch-node">
  <Handle type="target" position={Position.Top} id="input" />

  <div class="header">{data.label}</div>

  <div class="cases">
    {#each data.cases as caseLabel, index}
      <div class="case">
        {caseLabel}
        <Handle
          type="source"
          position={Position.Right}
          id="case-{index}"
          style="top: {30 + index * 28}px"
        />
      </div>
    {/each}
  </div>
</div>

<style>
  .switch-node {
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    padding: 12px;
    min-width: 150px;
  }

  .header {
    font-weight: bold;
    text-align: center;
    border-bottom: 1px solid #eee;
    padding-bottom: 8px;
    margin-bottom: 8px;
  }

  .cases {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .case {
    position: relative;
    font-size: 14px;
    text-align: right;
    padding-right: 16px;
  }
</style>

可调整大小的节点

<!-- ResizableNode.svelte -->
<script lang="ts">
  import { Handle, Position, NodeResizer } from '@xyflow/svelte';

  export let id: string;
  export let data: { label: string; content: string };
  export let selected: boolean;
</script>

<NodeResizer
  color="#ff0071"
  isVisible={selected}
  minWidth={100}
  minHeight={50}
  handleStyle={{ width: '8px', height: '8px' }}
/>

<Handle type="target" position={Position.Top} />

<div class="content">
  <div class="label">{data.label}</div>
  <div class="body">{data.content}</div>
</div>

<Handle type="source" position={Position.Bottom} />

<style>
  .content {
    padding: 16px;
    height: 100%;
    background: white;
    border: 1px solid #1a192b;
    border-radius: 8px;
  }

  .label {
    font-weight: bold;
    margin-bottom: 8px;
  }

  .body {
    font-size: 14px;
    color: #666;
  }
</style>

带工具栏的节点

<!-- EditableNode.svelte -->
<script lang="ts">
  import { Handle, Position, NodeToolbar, useSvelteFlow } from '@xyflow/svelte';
  import { createEventDispatcher } from 'svelte';

  export let id: string;
  export let data: { label: string };
  export let selected: boolean;

  const { setNodes, deleteElements } = useSvelteFlow();
  const dispatch = createEventDispatcher();

  let isEditing = false;
  let editValue = data.label;

  function handleEdit() {
    isEditing = true;
  }

  function handleSave() {
    setNodes((nodes) =>
      nodes.map((node) =>
        node.id === id
          ? { ...node, data: { ...node.data, label: editValue } }
          : node
      )
    );
    isEditing = false;
  }

  function handleDelete() {
    deleteElements({ nodes: [{ id }] });
  }
</script>

<NodeToolbar isVisible={selected} position={Position.Top}>
  <button on:click={handleEdit} class="toolbar-btn">✏️ 编辑</button>
  <button on:click={handleDelete} class="toolbar-btn delete">🗑️ 删除</button>
</NodeToolbar>

<Handle type="target" position={Position.Top} />

<div class="node-content">
  {#if isEditing}
    <div class="edit-form">
      <input bind:value={editValue} class="nodrag" />
      <button on:click={handleSave}>保存</button>
    </div>
  {:else}
    <span>{data.label}</span>
  {/if}
</div>

<Handle type="source" position={Position.Bottom} />

<style>
  .node-content {
    padding: 12px 16px;
    background: white;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }

  .toolbar-btn {
    padding: 4px 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
    background: white;
    cursor: pointer;
  }

  .toolbar-btn.delete {
    color: #dc2626;
  }

  .edit-form {
    display: flex;
    gap: 8px;
  }

  input {
    padding: 4px 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }
</style>

自定义边

<!-- ButtonEdge.svelte -->
<script lang="ts">
  import {
    BaseEdge,
    EdgeLabelRenderer,
    getBezierPath,
    useSvelteFlow,
  } from '@xyflow/svelte';

  export let id: string;
  export let sourceX: number;
  export let sourceY: number;
  export let targetX: number;
  export let targetY: number;
  export let sourcePosition: Position;
  export let targetPosition: Position;
  export let style: string = '';
  export let markerEnd: string = '';

  const { setEdges } = useSvelteFlow();

  $: [edgePath, labelX, labelY] = getBezierPath({
    sourceX,
    sourceY,
    sourcePosition,
    targetX,
    targetY,
    targetPosition,
  });

  function handleClick() {
    setEdges((edges) => edges.filter((edge) => edge.id !== id));
  }
</script>

<BaseEdge path={edgePath} {markerEnd} {style} />
<EdgeLabelRenderer>
  <div
    style="
      position: absolute;
      transform: translate(-50%, -50%) translate({labelX}px, {labelY}px);
      pointer-events: all;
    "
    class="nodrag nopan"
  >
    <button class="delete-button" on:click={handleClick}>×</button>
  </div>
</EdgeLabelRenderer>

<style>
  .delete-button {
    width: 20px;
    height: 20px;
    background: #f0f0f0;
    border-radius: 50%;
    border: 1px solid #999;
    cursor: pointer;
    font-size: 14px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .delete-button:hover {
    background: #ffcccc;
  }
</style>

注册自定义边

<script lang="ts">
  import { SvelteFlow, type EdgeTypes } from '@xyflow/svelte';
  import { writable } from 'svelte/store';
  import ButtonEdge from './ButtonEdge.svelte';
  import AnimatedEdge from './AnimatedEdge.svelte';

  const edgeTypes: EdgeTypes = {
    buttonEdge: ButtonEdge,
    animated: AnimatedEdge,
  };

  const edges = writable([
    {
      id: 'e1-2',
      source: '1',
      target: '2',
      type: 'buttonEdge',
    },
  ]);
</script>

<SvelteFlow {nodes} {edges} {edgeTypes} />

组节点(父容器)

<!-- GroupNode.svelte -->
<script lang="ts">
  export let data: { label: string };
</script>

<div class="group-node">
  <div class="group-label">{data.label}</div>
</div>

<style>
  .group-node {
    padding: 8px;
    border: 2px dashed #999;
    border-radius: 8px;
    background: rgba(240, 240, 240, 0.5);
    min-width: 200px;
    min-height: 150px;
  }

  .group-label {
    font-size: 12px;
    color: #666;
    font-weight: 500;
  }
</style>
<!-- 带有子节点的用法 -->
<script>
  const nodes = writable([
    {
      id: 'group-1',
      type: 'group',
      data: { label: '组 A' },
      position: { x: 0, y: 0 },
      style: 'width: 300px; height: 200px;',
    },
    {
      id: 'child-1',
      data: { label: '子节点' },
      position: { x: 50, y: 50 },
      parentId: 'group-1',
      extent: 'parent',
    },
  ]);
</script>

自定义句柄组件

<!-- CustomHandle.svelte -->
<script lang="ts">
  import { Handle } from '@xyflow/svelte';

  export let type: 'source' | 'target';
  export let position: Position;
  export let id: string = '';
  export let isConnectable: boolean = true;
  export let color: string = '#555';
</script>

<Handle
  {type}
  {position}
  {id}
  {isConnectable}
  style="
    width: 12px;
    height: 12px;
    background: {color};
    border: 2px solid white;
  "
/>

带有状态的交互式节点

<!-- CounterNode.svelte -->
<script lang="ts">
  import { Handle, Position, useSvelteFlow } from '@xyflow/svelte';

  export let id: string;
  export let data: { count: number };

  const { setNodes } = useSvelteFlow();

  function increment() {
    setNodes((nodes) =>
      nodes.map((node) =>
        node.id === id
          ? { ...node, data: { ...node.data, count: node.data.count + 1 } }
          : node
      )
    );
  }

  function decrement() {
    setNodes((nodes) =>
      nodes.map((node) =>
        node.id === id
          ? { ...node, data: { ...node.data, count: node.data.count - 1 } }
          : node
      )
    );
  }
</script>

<div class="counter-node">
  <Handle type="target" position={Position.Top} />

  <div class="display">{data.count}</div>

  <div class="buttons">
    <button on:click={decrement} class="nodrag">-</button>
    <button on:click={increment} class="nodrag">+</button>
  </div>

  <Handle type="source" position={Position.Bottom} />
</div>

<style>
  .counter-node {
    background: white;
    border: 2px solid #1a192b;
    border-radius: 12px;
    padding: 16px;
    text-align: center;
  }

  .display {
    font-size: 32px;
    font-weight: bold;
    margin: 8px 0;
  }

  .buttons {
    display: flex;
    gap: 8px;
    justify-content: center;
  }

  button {
    width: 32px;
    height: 32px;
    border: none;
    border-radius: 50%;
    background: #1a192b;
    color: white;
    font-size: 18px;
    cursor: pointer;
  }

  button:hover {
    background: #333;
  }
</style>

CSS 样式

/* 全局流样式 */
:global(.svelte-flow__node-custom) {
  background: white;
  border: 1px solid #1a192b;
  border-radius: 8px;
  padding: 10px;
  font-size: 12px;
}

:global(.svelte-flow__node-custom.selected) {
  border-color: #ff0071;
  box-shadow: 0 0 0 2px #ff0071;
}

:global(.svelte-flow__handle) {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background-color: #555;
}

:global(.svelte-flow__handle-connecting) {
  background-color: #ff0071;
}

:global(.svelte-flow__handle-valid) {
  background-color: #55dd99;
}

/* 防止交互元素拖拽 */
:global(.nodrag) {
  pointer-events: all;
}

/* 边样式 */
:global(.svelte-flow__edge-path) {
  stroke: #b1b1b7;
  stroke-width: 2;
}

:global(.svelte-flow__edge.selected .svelte-flow__edge-path) {
  stroke: #ff0071;
}

何时使用此技能

使用 svelteflow-custom-nodes 当您需要:

  • 构建具有交互式 Svelte 组件的节点
  • 创建视觉上不同的节点类型
  • 添加节点工具栏和上下文菜单
  • 构建可调整大小的节点
  • 创建组/容器节点
  • 添加自定义边交互
  • 在 Svelte 中构建复杂的工作流界面

最佳实践

  • 使用 Svelte 的反应性进行状态管理
  • 在组件级别定义 nodeTypes/edgeTypes
  • 在交互元素上使用 nodrag
  • 保持节点组件专注且可重用
  • 使用 TypeScript 实现类型安全的节点数据
  • 使用 CSS 作用域进行节点样式
  • 彻底测试连接验证

资源