自定义节点(#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 透传中通常只显式传 node、checked、dragging |
checked 和 selected 的区别:
| 字段/参数 | 来源 | 含义 | 常见用途 |
|---|---|---|---|
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.disablePointEvent 或 node.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.color、node.borderColor、node.width |
写入外层 CSS 变量,主图和部分内部视图可保持一致 |
| 全局配置 | defaultNodeColor、defaultNodeBorderWidth |
节点字段未设置时的默认值 |
| 插槽 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;
}
说明:
- 如果节点数据中已经设置了
color、borderColor、borderWidth等字段,在插槽样式中使用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/y、selected、expanded 这类图谱结构/状态字段塞进 node.data。
能不能完全绕过默认节点外壳?
常规 #node 不能。它只替换 .rg-node 内部内容。这样做是为了保留节点拖拽、选中、尺寸测量、连接点计算和内部状态。如果你要实现完全自定义渲染,通常需要在更高层自定义图谱核心或修改源码。