JavaScript is required

自定义节点(#node

节点插槽用于替换节点内部内容。它不会替换整个节点外壳:节点的定位、显示/隐藏、拖拽入口、选中状态 class、尺寸监听、默认 CSS 变量仍由 relation-graph 的 RGNodePeel 维护。

这点很重要:你在 #node 中写的是 .rg-node 内部的内容,而不是最外层 .rg-node-peel。所以通常不需要在插槽里处理 transform: translate(node.x, node.y),也不需要自己把节点注册到图谱实例中。

1. 各平台写法

平台 写法 说明
Vue 3 / Vue 2 <template #node="{ node, checked, dragging }"> 推荐写法
React <RGSlotOnNode>{props => ...}</RGSlotOnNode> children 必须是函数
React nodeSlot={({ node }) => ...} 属性式写法,不能和 <RGSlotOnNode> 同时使用
Svelte <div slot="node" let:node let:checked let:dragging> 使用 Svelte named slot

2. 插槽参数

源码中的类型定义:

export type RGNodeSlotProps = {
  node: RGNode;
  defaultExpandHolderPosition?: string;
  dragging?: boolean;
  checked?: boolean;
};
参数 类型 作用
node RGNode 当前渲染的运行时节点对象。包含节点 ID、文本、类型、位置、尺寸、样式、业务数据和运行时状态
checked boolean | undefined 当前节点是否被内部控制器标记为 checked。它通常用于编辑控制器、连接控制器、临时高亮等内部状态
dragging boolean | undefined 当前节点是否正在被拖拽
defaultExpandHolderPosition string | undefined 全局默认展开按钮位置,来源于 options.defaultExpandHolderPosition;当前 Vue/Svelte 的普通 #node 透传中通常只显式传 nodecheckeddragging

checkedselected 的区别:

字段/参数 来源 含义 常见用途
node.selected RGNode 数据状态 节点被用户或 API 选中 业务选中态、导出/持久化可能关注
checked 渲染时参数 内部控制器的当前目标节点 编辑、连线、临时操作态
dragging 渲染时参数 当前节点是否正在拖拽 拖拽中的视觉反馈

3. node 对象中常用字段

完整节点数据请参考 节点数据模型。在节点插槽中最常使用这些字段:

字段 类型 用途
node.id string 节点唯一 ID,常用于事件、测试选择器、业务操作
node.text string | undefined 默认显示文本
node.type string | undefined 节点类型。推荐用它分发不同节点模板
node.data Record<string, any> | undefined 业务数据容器。用户头像、状态、数量、权限等业务字段建议放这里
node.color string | undefined 节点主背景色,会写入 --rg-node-color
node.fontColor string | undefined 节点文字颜色,会写入 --rg-node-font-color
node.borderColor string | undefined 节点边框颜色,会写入 --rg-node-border-color
node.borderWidth number | undefined 节点边框宽度,单位 px
node.borderRadius number | undefined 节点圆角,单位 px
node.width / node.height number | undefined 节点固定宽高,单位 px;不设置时由内容撑开
node.x / node.y number 画布坐标,由外层节点壳负责应用
node.expanded boolean | undefined 树形/层级数据中的展开状态,false 表示收起
node.rgChildrenSize number 运行时统计的子节点数量,默认展开按钮会依赖它判断是否显示
node.className string | undefined 附加到外层 .rg-node-peel 上的 class
node.disablePointEvent boolean | undefined 禁用节点事件。启用后外层会加 .rg-node-disable-events

4. 外层结构与默认 class

当前 Vue3/React/Svelte 的节点渲染结构可以概括为:

<div class="rg-node-peel rg-node-selected rg-node-shape-1 rg-node-type-user" data-id="node-id">
  <!-- 可选:node-expand-button -->
  <div class="rg-node">
    <!-- 你的 #node 内容在这里 -->
  </div>
</div>

外层 .rg-node-peel 会处理:

内容 当前行为
定位 使用 transform: translate(node.x, node.y)
可见性 根据 node.rgCalcedVisibility 显示/隐藏
选中 class node.selected 时添加 .rg-node-selected
拖拽 class dragging 为真时添加 .rg-node-dragging
checked class checked 为真时添加 .rg-node-checked
节点形状 class 添加 .rg-node-shape-${node.nodeShape},默认矩形为 1
节点类型 class 添加 .rg-node-type-${node.type}
自定义 class 添加 node.className
事件禁用 class node.disablePointEventnode.opacity === 0 时添加 .rg-node-disable-events
样式变量 写入节点颜色、字体、边框、宽高、透明度等 CSS 变量
尺寸监听 节点挂载后调用实例的 resize listener,同步节点实际宽高

因此节点插槽中建议只关注内容结构,不要重复实现外层状态逻辑。

5. 基础示例

Vue 3

<script setup lang="ts">
import { RelationGraph } from '@relation-graph/vue';
import type { RGNode } from '@relation-graph/vue';

function openNode(node: RGNode, event: MouseEvent) {
  event.stopPropagation();
  console.log('open node:', node.id);
}
</script>

<template>
  <RelationGraph :options="graphOptions" :initial-data="graphData">
    <template #node="{ node, checked, dragging }">
      <div
        class="user-node"
        :class="{
          'is-selected': node.selected,
          'is-checked': checked,
          'is-dragging': dragging
        }"
      >
        <img v-if="node.data?.avatar" class="avatar" :src="node.data.avatar" />
        <div class="main">
          <div class="title">{{ node.text }}</div>
          <div class="desc">{{ node.data?.role || '未设置角色' }}</div>
        </div>
        <button class="rg-events-all" @click="openNode(node, $event)">详情</button>
      </div>
    </template>
  </RelationGraph>
</template>

React

import {
  RelationGraph,
  RGSlotOnNode,
  type RGNode
} from '@relation-graph/react';

function UserNode({ node, checked, dragging }: {
  node: RGNode;
  checked?: boolean;
  dragging?: boolean;
}) {
  return (
    <div
      className={[
        'user-node',
        node.selected ? 'is-selected' : '',
        checked ? 'is-checked' : '',
        dragging ? 'is-dragging' : ''
      ].filter(Boolean).join(' ')}
    >
      {node.data?.avatar && <img className="avatar" src={node.data.avatar} />}
      <div className="main">
        <div className="title">{node.text}</div>
        <div className="desc">{node.data?.role || '未设置角色'}</div>
      </div>
      <button
        className="rg-events-all"
        onMouseDown={(e) => e.stopPropagation()}
        onClick={(e) => {
          e.stopPropagation();
          console.log('open node:', node.id);
        }}
      >
        详情
      </button>
    </div>
  );
}

<RelationGraph options={graphOptions} initialData={graphData}>
  <RGSlotOnNode>
    {(props) => <UserNode {...props} />}
  </RGSlotOnNode>
</RelationGraph>

Svelte

<RelationGraph {options} initialData={graphData}>
  <div
    slot="node"
    let:node
    let:checked
    let:dragging
    class:is-selected={node.selected}
    class:is-checked={checked}
    class:is-dragging={dragging}
    class="user-node"
  >
    {#if node.data?.avatar}
      <img class="avatar" src={node.data.avatar} alt="" />
    {/if}
    <div class="main">
      <div class="title">{node.text}</div>
      <div class="desc">{node.data?.role || '未设置角色'}</div>
    </div>
  </div>
</RelationGraph>

6. 按 node.type 分发模板

对于复杂业务图谱,不建议在一个节点模板中堆大量 if。更推荐按 node.type 分发。

<template #node="{ node, checked, dragging }">
  <UserNode
    v-if="node.type === 'user'"
    :node="node"
    :checked="checked"
    :dragging="dragging"
  />
  <OrgNode
    v-else-if="node.type === 'org'"
    :node="node"
    :checked="checked"
    :dragging="dragging"
  />
  <DefaultNode
    v-else
    :node="node"
    :checked="checked"
    :dragging="dragging"
  />
</template>

这样做的好处:

  • 节点类型和视觉模板一一对应,维护成本更低。
  • node.data 可以保持业务含义清晰,不需要为视觉结构制造大量临时字段。
  • 同一类节点可以复用局部组件、测试和样式。

7. 自定义展开按钮

默认展开按钮只在满足条件时显示:

条件 当前行为
node.expandHolderPosition 有值且不为 hide 显示展开按钮
node.expandHolderPosition 未设置,options.defaultExpandHolderPosition 不为 hide,且 node.rgChildrenSize > 0 显示展开按钮
位置为 hide 不显示

可选位置来自当前样式实现:

说明
left 显示在节点左侧
right 显示在节点右侧
top 显示在节点上方
bottom 显示在节点下方
hide 隐藏展开按钮

Vue 示例:

<template #node-expand-button="{ node, expandOrCollapseNode, expandHolderPosition }">
  <button
    class="my-expand rg-events-all"
    :class="'pos-' + expandHolderPosition"
    @click.stop="expandOrCollapseNode"
  >
    {{ node.expanded === false ? '+' : '-' }}
  </button>
</template>

React 示例:

<RelationGraph
  options={graphOptions}
  initialData={graphData}
  nodeExpandButtonSlot={({ node, expandOrCollapseNode, expandHolderPosition }) => (
    <button
      className={`my-expand pos-${expandHolderPosition} rg-events-all`}
      onMouseDown={(e) => e.stopPropagation()}
      onClick={expandOrCollapseNode}
    >
      {node.expanded === false ? '+' : '-'}
    </button>
  )}
/>

注意:expandOrCollapseNode 已经调用内部展开/收起逻辑,通常不需要你自己直接修改 node.expanded

8. 样式建议

节点插槽的外观通常由三层共同决定:

来源 示例 影响
节点数据 node.colornode.borderColornode.width 写入外层 CSS 变量,主图和部分内部视图可保持一致
全局配置 defaultNodeColordefaultNodeBorderWidth 节点字段未设置时的默认值
插槽 CSS .user-node .avatar.user-node .badge 复杂内容布局和业务装饰

推荐 CSS:

.user-node {
  min-width: 160px;
  min-height: 56px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  box-sizing: border-box;
  background: var(--rg-node-color);
  color: var(--rg-node-font-color);
  border: var(--rg-node-border-width) solid var(--rg-node-border-color);
  border-radius: var(--rg-node-border-radius);
  font-size: var(--rg-node-font-size);
}

.user-node.is-selected {
  box-shadow: 0 0 0 2px #3b82f6;
}

.user-node.is-dragging {
  opacity: 0.75;
}

.user-node .avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  object-fit: cover;
}

说明:

  • 如果节点数据中已经设置了 colorborderColorborderWidth 等字段,在插槽样式中使用 var(--rg-node-*) 可以保持数据语义和视觉一致。
  • 如果你完全在插槽 CSS 中硬编码背景/边框,主视图可以正常显示,但缩略图、导出、性能模式或其他内部渲染可能无法表达同样的视觉语义。
  • 固定宽高的节点建议同时设置 node.width/node.height 或 CSS 明确尺寸,避免异步图片加载后节点尺寸突然变化。

9. 节点内交互控件

节点外层 .rg-node 会监听 mousedown / touchstart 并启动节点拖拽。节点内按钮、输入框、菜单如果不希望触发拖拽,需要阻止事件冒泡。

Vue:

<button class="rg-events-all" @mousedown.stop @click.stop="openPanel(node)">
  编辑
</button>

React:

<button
  className="rg-events-all"
  onMouseDown={(e) => e.stopPropagation()}
  onTouchStart={(e) => e.stopPropagation()}
  onClick={(e) => {
    e.stopPropagation();
    openPanel(node);
  }}
>
  编辑
</button>

Svelte:

<button
  class="rg-events-all"
  on:mousedown|stopPropagation
  on:touchstart|stopPropagation
  on:click|stopPropagation={() => openPanel(node)}
>
  编辑
</button>

如果控件还需要接收拖拽、输入、滚动等事件,也要检查它所在层级是否受到 pointer-events 影响;节点插槽通常可直接交互,画布和视图层需要额外开启事件。

10. 常见问题

为什么我在 #node 里设置 left/top 没有效果?

节点定位由外层 .rg-node-peel 负责,使用的是 transform: translate(node.x, node.y)#node 只控制节点内部内容,通常不应该自己定位整个节点。

为什么自定义节点拖不动?

常见原因是插槽内容覆盖了节点外层事件,或者内部元素阻止了 mousedown/touchstart 冒泡。普通内容不要随意 stopPropagation,只在按钮、输入框、菜单等交互控件上阻止。

为什么节点实际连线位置不准?

节点尺寸由 DOM 测量同步到运行时。异步图片、字体加载、折叠内容展开可能改变尺寸。建议:

  • 给图片设置固定宽高。
  • 给复杂节点设置稳定的 width/height
  • 数据变化后必要时调用实例的视图/布局更新 API。

node.data 应该放什么?

放业务字段,例如用户头像、状态、分组、权限、统计数、外部 ID。不要把 x/yselectedexpanded 这类图谱结构/状态字段塞进 node.data

能不能完全绕过默认节点外壳?

常规 #node 不能。它只替换 .rg-node 内部内容。这样做是为了保留节点拖拽、选中、尺寸测量、连接点计算和内部状态。如果你要实现完全自定义渲染,通常需要在更高层自定义图谱核心或修改源码。

11. 下一步阅读