JavaScript is required

画布交互事件

本示例构建了一个全高的 relation-graph 查看器,并把画布本身作为主要关注对象。图数据刻意保持很小,但页面额外加入了一个可拖拽的辅助窗口、固定的 toast 提示、内置工具栏,以及用户拖动画布时出现的临时红色箭头叠加层。

图工作区中的画布拖拽反馈与选中清除

这个示例构建了什么

本示例构建了一个全高的 relation-graph 查看器,并把画布本身作为主要关注对象。图数据刻意保持很小,但页面额外加入了一个可拖拽的辅助窗口、固定的 toast 提示、内置工具栏,以及用户拖动画布时出现的临时红色箭头叠加层。

用户可以点击空白画布区域来清除 checked 状态,拖动画布时查看实时反馈,在浮动窗口中打开设置面板,在运行时切换滚轮和拖拽行为,并将当前图视图导出为图片。最有价值的部分在于,画布回调不再只是停留在控制台中的事件,而是变成了可见的界面行为。

数据如何组织

图数据在 MyGraph.tsxinitializeGraph() 中以内联方式声明为一个 RGJsonData 对象。它使用 rootId: 'a',定义了四个节点,并声明了四条带有显式 ID 的连线记录,例如 l1l4

在调用 setJsonData(...) 之前没有额外的预处理流程,只有把展示元数据直接嵌入数据集。节点颜色、字体颜色、形状、宽度和高度都以内联方式指定,连线也带有内联标签和颜色。在真实应用中,同样的结构可以表示紧凑的设备拓扑、工作流检查点地图,或任何更关注交互处理而不是自定义数据加载的小型图。

如何使用 relation-graph

index.tsx 使用 RGProvider 包装这个 demo,MyGraph.tsx 则通过 RGHooks.useGraphInstance() 获取当前图实例。该示例没有定义显式布局选项,因此在通过 setJsonData(...) 加载内联数据之后,它依赖 relation-graph 的默认行为,再使用 moveToCenter()zoomToFit() 将结果居中并适配视图。

图选项虽然不多,但都很关键。showToolBar: true 让内置工具栏保持可见,checkedItemBackgroundColor 为被 checked 的对象提供半透明绿色高亮,defaultLineWidth: 1 让连线保持较细,而 defaultJunctionPoint: RGJunctionPoint.border 让连线端点附着在节点边框上。RelationGraph 绑定了 onCanvasClickonCanvasDragStartonCanvasDraggingonCanvasDragEnd,而节点和连线的点击处理器只用于控制台日志输出。

大部分可复用行为都来自图实例 API。onCanvasClick 会读取 getCheckedNode()getCheckedLine(),显示一个 toast,并调用 clearChecked()onCanvasDragStart 通过 getViewXyByClientXy(...) 把浏览器坐标转换为图视图坐标,随后 React state 同时驱动辅助窗口中的读数和 MyArrowLayer SVG 叠加层。浮动的 DraggableWindow 是本地 React 组件,而不是 relation-graph slot,但它的设置面板仍然通过 RGHooks.useGraphStore() 反映 wheelEventActiondragEventAction,通过 setOptions(...) 在运行时切换这些模式,并使用 prepareForImageGeneration()restoreAfterImageGeneration() 支持截图导出。这个示例没有使用节点、连线、画布或视口 slot;拖拽指示器是一个单独的绝对定位叠加组件。

my-relation-graph.scss 中的样式刻意保持轻量,基本上让 relation-graph 的选择器维持开放。可见的定制主要来自节点和连线的内联展示数据、共享的浮动窗口组件,以及右上角 toast UI 的样式。

关键交互

  • 点击空白画布区域时,会检查当前是否有节点或连线处于 checked 状态,显示成功或警告 toast,然后清除该 checked 状态。
  • 开始拖动画布时,会保存一个相对于图的起点,因此反馈叠加层锚定的是 relation-graph 视图坐标,而不是原始浏览器坐标。
  • 拖拽过程中会更新实时偏移状态,并同时在辅助窗口中显示文本读数,以及在图上显示临时红色箭头。
  • 结束拖拽时,会移除当前拖拽状态并显示一个完成 toast。
  • 辅助窗口可以被拖动、最小化、展开为设置面板、在滚轮和拖拽模式之间切换,并用于下载当前图视图的图片。

关键代码片段

下面这段展示了示例图以内联方式声明,并在挂载期间加载,随后将视图居中并适配:

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'A' },
        { id: 'b', text: 'B', color: '#43a2f1', fontColor: 'yellow' },
        { id: 'c', text: 'C', nodeShape: RGNodeShape.rect, width: 80, height: 60 },
        { id: 'e', text: 'E', nodeShape: RGNodeShape.circle, width: 150, height: 150 }
    ],
    // ...
};
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();

下面这段展示了空白画布点击如何把 relation-graph 的 checked 状态转化为可见反馈,然后再重置:

const onCanvasClick = ($event: RGUserEvent) => {
    console.log('onCanvasClick:', $event);
    if (graphInstance.getCheckedNode() || graphInstance.getCheckedLine()) {
        SimpleGlobalMessage.success('Clear Line/Node Checked Status');
    } else {
        SimpleGlobalMessage.warning('Please Check Node Or Line');
    }
    graphInstance.clearChecked();
};

下面这段展示了拖拽生命周期如何捕获相对于图的起点以及实时拖拽偏移:

const onCanvasDragStart = (canvasMoveStartPosition: RGPosition, eventClientStartPosition: RGPosition, e: RGUserEvent) => {
    SimpleGlobalMessage.success('Canvas Drag Start');
    setCanvasDragging(true);
    const xyOnView = graphInstance.getViewXyByClientXy(eventClientStartPosition);
    setCanvasDraggingStartPosition(xyOnView);
};
const onCanvasDragging = (canvasOffsetX: number, canvasOffsetY: number, buffX: number, buffY: number) => {
    setCanvasDraggingInfo({ buffX, buffY });
};

下面这段展示了可视化拖拽指示器是一个独立的 SVG 图层,它根据保存的偏移量计算终点,并在原点处消失:

const endX = startPos.x + offset.buffX;
const endY = startPos.y + offset.buffY;
if (offset.buffX === 0 && offset.buffY === 0) return null;

<line
    x1={startPos.x}
    y1={startPos.y}
    x2={endX}
    y2={endY}
    stroke={color}
    markerEnd="url(#arrowhead-solid)"
/>

下面这段展示了浮动设置面板中运行时切换画布模式的写法:

<SettingRow
    label="Wheel Event:"
    options={[
        { label: 'Scroll', value: 'scroll' },
        { label: 'Zoom', value: 'zoom' },
        { label: 'None', value: 'none' },
    ]}
    value={wheelMode}
    onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>

下面这段展示了导出流程如何让 relation-graph 先准备画布 DOM,将其渲染为图片 blob,然后恢复图状态:

const canvasDom = await graphInstance.prepareForImageGeneration();
let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
    graphBackgroundColor = '#ffffff';
}
const imageBlob = await domToImageByModernScreenshot(canvasDom, {
    backgroundColor: graphBackgroundColor
});
await graphInstance.restoreAfterImageGeneration();

这个示例的独特之处

对比数据认为 drag-and-wheel-event 与这个示例最接近,因为这两个页面都复用了同一个浮动辅助窗口、运行时画布设置和图片导出工具。区别在于,canvas-event 把拖拽生命周期本身作为核心教学内容。它绑定了 onCanvasDragStartonCanvasDraggingonCanvasDragEnd,通过 getViewXyByClientXy(...) 转换拖拽开始时的浏览器位置,并把这些状态变成实时箭头叠加层。

它与 canvas-selectionselections 也有明显不同。那些示例会把拖拽手势用于框选或图编辑行为,而这个示例从不把拖拽变成选择、节点创建或图变更。这里的拖拽手势只产生短暂反馈:读数、toast 和红色矢量叠加层。

node-line-tips-contentmenu 相比,这个示例强调的是整个画布,而不是特定对象。结合稀有度和对比数据支持的这组少见组合包括:空白画布 checked 状态清除、图坐标转换、运行时切换滚轮和拖拽模式、截图导出,以及在一个紧凑的混合形状示例图上显示临时拖拽方向叠加层。浮动辅助窗口和导出路径属于共享基础设施,但真正让这个示例更适合作为工作区交互工具起点的,是它的拖拽仪表化模式。

这种模式还能用在哪里

这种模式很适合迁移到那些需要用户理解画布手势、而不只是查看图内容的图工作台中。例如拓扑审查界面、运维仪表盘、类似地图的图查看器、用于讲解平移和缩放规则的引导沙盒,以及用于调试自定义交互策略的工具。

当团队希望在现有只读图之上叠加仪表化能力,而不把页面变成完整编辑器时,它也同样有用。相同的方法还可以扩展为拖拽引导、临时测量叠加层、手势训练提示,或用于验证不同画布模式在实时交互下表现的 QA 界面。