赞
踩
目录
博主最近的需要实现一个前端绘制拓扑图的工具,并且要求能够编辑节点的信息,以及将拓扑结构传给后端。首先想到的就是Jsplumb,这边记录一下开发过程中的一些知识点,本文中的代码都是基于vue3+ts写的。
先给大家推荐一些写的不错的文章和文档。
jsPlumb 基本概念 - 简书 (jianshu.com)
Overview | jsPlumb Documentation (jsplumbtoolkit.com)
一句话来说就是一个在web端绘制关系图的开源工具。
安装很简单,直接使用以下命令即可
npm install jsplumb --save
导入工具
- <script lang="ts" setup>
- import { jsPlumb } from 'jsplumb';
- import type { jsPlumbInstance } from 'jsplumb';
- import { onMounted } from 'vue';
-
- let jsPlumb_instance: jsPlumbInstance = null;
-
- onMounted(() => {
- jsPlumb_instance = jsPlumb.getInstance();
-
- jsPlumb_instance.ready(function() {
- /* code */
- };)
- })
- </script>
这里要注意,ready函数是jsplumb的初始化函数,由于jsplumb是基于dom元素的,因此在dom元素挂载上之前jsplumb是无法工作的,所以需要将ready函数放在onMounted函数下。而getInstance函数可以选择放在onMounted也可以放在onBeforeMount中。
官方文档用一句话概括了这些元素
In summary - one
Connection
is made up of twoEndpoint
s, aConnector
, and zero or moreOverlay
s working together to join two elements. EachEndpoint
has an associatedAnchor
.
翻译过来就是:
总之,一个Connection由两个端点(endpoint)、一个Connector和零个或多个overlay组成,它们一起连接两个元素。每个端点都有一个关联的锚。
创建一个实例
- <script lang="ts" setup>
- import { jsPlumb } from 'jsplumb';
- import type { jsPlumbInstance } from 'jsplumb';
- import { onMounted } from 'vue';
-
- let jsPlumb_instance: jsPlumbInstance = null;
-
- onMounted(() => {
- jsPlumb_instance = jsPlumb.getInstance();
- })
- </script>
向已有元素增加端点。
- <script lang="ts" setup>
- ...
- import type { EndpointOptions } from "jsplumb";
- ...
-
- ...
- let el = <Element>(document.getElementById("node1");
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isTarget: true,
- isSource: true,
- anchor: "Top"
- });
- </script>
设置元素是否可拖拽,并可以限制拖拽区域。
- <script lang="ts" setup>
- ...
- import type { DragOptions} from "jsplumb";
- ...
-
- ...
- let el = <Element>(document.getElementById("node1");
- jsPlumb_instance.draggable(el, <DragOptions>{
- containment: "jsplumb_canvas"
- });
- </script>
jsplumb的功能还是很强大的,这里不可能完全罗列出来,也没有必要,但是为了帮助大家更快的上手jsplumb的开发,我在这里提几个建议。
首先给出数据,说明一下数据的含义:id、name、value都是指当前节点的属性,in_id表示指向当前节点的另一个节点的id。input表示输入节点的id,output表示的节点会指向一个输出节点。
- let input: number[] = [-1];
- let output: number[] = [6, 7];
- let blocks: PlumbBlock[] = [
- {
- id: 0,
- in_id: [-1],
- name: "block",
- value: ["3"]
- },
- {
- id: 1,
- in_id: [0],
- name: "block-long",
- value: ["32"]
- },
- {
- id: 2,
- in_id: [1],
- name: "block",
- value: ["64"]
- },
- {
- id: 3,
- in_id: [2],
- name: "block-long",
- value: ["64"]
- },
- {
- id: 4,
- in_id: [1, 3],
- name: "block",
- value: ["128"]
- },
- {
- id: 5,
- in_id: [4],
- name: "block-long",
- value: ["128"]
- },
- {
- id: 6,
- in_id: [2, 5],
- name: "block",
- value: ["256"]
- },
- {
- id: 7,
- in_id: [6],
- name: "block-long",
- value: ["256"]
- }
- ];
可以看到input和output节点的表示与其他blocks是不同的,并且为了展示的时候这些节点是分开的而不是堆叠在一起,这边需要先做一个数据格式的转换,生成node_list和line_list两个列表,第一个列表包含所有的节点信息(包含了节点的位置偏移信息),第二个列表表示节点之间的连接关系。
- export interface PlumbBlock {
- id: number;
- name: string;
- in_id?: number[] | undefined;
- value?: string[];
- top?: string;
- };
-
- let node_list: PlumbBlock[] = [];
- let line_list: number[][] = []; // [Source, Target]
-
- function setNodeInfo(): void {
- let count: number = 0;
- let step: number = 75;
-
- for(let i = 0; i < input.length; ++i) {
- node_list.push({
- id: input[i],
- name: "input",
- top: count * step + "px",
- });
- ++count;
- };
-
- for(let i = 0; i < blocks.length; ++i) {
- node_list.push({
- id: blocks[i].id,
- in_id: blocks[i].in_id,
- name: blocks[i].name,
- value: blocks[i].value,
- top: count * step + "px",
- });
- ++count;
-
- let in_id: number[] | undefined = blocks[i].in_id;
- if(in_id != undefined) {
- for(let j = 0; j < in_id.length; ++j)
- line_list.push([in_id[j], blocks[i].id);
- }
- };
-
- for(let i = 0; i < output.length; ++i) {
- let id = blocks.length + i;
- node_list.push({
- id: id,
- name: "output",
- top: count * step + "px",
- });
- ++count;
-
- line_list.push([output[i], id);
- };
- };
这里使用一个v-for循环来渲染这些数据,也就是说给每个数据生成一个dom元素。这样jsplumb才能工作。
- <template>
- <div id="jsplumb" class="jsplumb">
- <div
- class="nodes"
- v-for="item in node_list"
- :key="item.id"
- :id="item.id"
- :style="{left: '40%', top: item.top}"
- >{{ item.name }}<div>
- </div>
- </template>
-
- <style scoped>
- .jsplumb {
- position: absolute,
- margin: 10px;
- left: 45%;
- width: 50%;
- height: 800px;
- box-sizing: bording-box;
- border: 1px solid black;
- }
- .nodes {
- position: absolute;
- min-width: 60px;
- height: 40px;
- color: black;
- border: 1px solid black;
- line-height: 40px;
- }
- </style>
这里注意两个踩坑点:
为每个节点添加端点,注意输入节点input只添加Source端点,输出节点output只添加Target端点,其他节点需要同时添加两类端点。
- function addEndpoints(): void {
- for(let i = 0; i < node_list.length; ++i) {
- let el = <Element>(document.getElementById(String(node_list[i].id)));
-
- jsPlumb_instance.draggable(el, <DragOptions>{ containment: "jsplumb" });
- if(node_list[i].name != "input") {
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isTarget: true,
- anchor: "Top",
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- });
- }
- if(node_list[i].name != "output") {
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isSource: true,
- anchor: "Bottom",
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- });
- }
- }
- };
这里可以看到我将一些公共属性提取出来了,作为一个参数传入,这是一个小技巧。
- let connect_common = {
- connector: ["Bezier", { curviness: 15 }],
- anchor: ["Top", "Bottom"],
-
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- endpointStyle: {
- stroke: "black",
- fill: "white",
- strokeWidth: 2,
- },
- };
-
- function drawLines(): void {
- for(let i = 0; i < line_list.length; ++i) {
- let start: Element = document.getElementById(String(line_list[i][0]));
- let end: Element = document.getElementById(String(line_list[i][1]));
-
- jsPlumb_instance.connect(
- {
- source: start,
- target: end,
- overlays: [["Arrow", { width: 12, length: 12, location: 0.5 }]],
- },
- connect_common
- );
- }
- };
这里也有两个坑要注意:
这样就可以显示拓扑结构了,并且可以随意拖拽这些节点。但是看到这里大家也会发现jsplumb一个比较大的问题,没有自动布局,其实也是有的,在jsplumbtoolkits中,但是需要付费。那如果不想花钱咋办呢,不要急,看下一章。
- <template>
- <div id="jsplumb" class="jsplumb">
- <div
- class="nodes"
- v-for="item in node_list"
- :key="item.id"
- :id="item.id"
- :style="{left: '40%', top: item.top}"
- >{{ item.name }}<div>
- </div>
- </template>
-
- <script lang="ts" setup>
- import { jsPlumb } from 'jsplumb';
- import type {
- Element,
- DragOptions,
- EndpointOptions,
- EndpointSpec,
- jsPlumbInstance,
- } from 'jsplumb';
- import { onBeforeonMount, onMounted } from 'vue';
-
- export interface PlumbBlock {
- id: number;
- name: string;
- in_id?: number[] | undefined;
- value?: string[];
- top?: string;
- };
-
- let jsPlumb_instance: jsPlumbInstance = null;
- let node_list: PlumbBlock[] = [];
- let line_list: number[][] = [];
-
- onBeforeonMount(() => {
- setNodeInfo();
- });
-
- onMounted(() => {
- jsPlumb_instance = jsPlumb.getInstance();
-
- jsPlumb_instance.ready(function() {
- addEndpoints();
- drawLines();
- });
- });
-
- let connect_common = {
- connector: ["Bezier", { curviness: 15 }],
- anchor: ["Top", "Bottom"],
-
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- endpointStyle: {
- stroke: "black",
- fill: "white",
- strokeWidth: 2,
- },
- };
- let input: number = -1;
- let output: number[] = [6, 7];
- let blocks: PlumbBlock[] = [
- {
- id: 0,
- in_id: [-1],
- name: "block",
- value: ["3"]
- },
- {
- id: 1,
- in_id: [0],
- name: "block-long",
- value: ["32"]
- },
- {
- id: 2,
- in_id: [1],
- name: "block",
- value: ["64"]
- },
- {
- id: 3,
- in_id: [2],
- name: "block-long",
- value: ["64"]
- },
- {
- id: 4,
- in_id: [1, 3],
- name: "block",
- value: ["128"]
- },
- {
- id: 5,
- in_id: [4],
- name: "block-long",
- value: ["128"]
- },
- {
- id: 6,
- in_id: [2, 5],
- name: "block",
- value: ["256"]
- },
- {
- id: 7,
- in_id: [6],
- name: "block-long",
- value: ["256"]
- }
- ];
-
- function setNodeInfo(): void {
- let count: number = 0;
- let step: number = 75;
-
- node_list.push({
- id: input,
- name: "input",
- top: count * step + "px",
- });
- ++count;
-
- for(let i = 0; i < blocks.length; ++i) {
- node_list.push({
- id: blocks[i].id,
- in_id: blocks[i].in_id,
- name: blocks[i].name,
- value: blocks[i].value,
- top: count * step + "px",
- });
- ++count;
-
- let in_id: number[] | undefined = blocks[i].in_id;
- if(in_id != undefined) {
- for(let j = 0; j < in_id.length; ++j)
- line_list.push([in_id[j], blocks[i].id);
- }
- };
-
- for(let i = 0; i < output.length; ++i) {
- let id = blocks.length + i;
- node_list.push({
- id: id,
- name: "output",
- top: count * step + "px",
- });
- ++count;
-
- line_list.push([output[i], id);
- };
- };
-
- function addEndpoints(): void {
- for(let i = 0; i < node_list.length; ++i) {
- let el = <Element>(document.getElementById(String(node_list[i].id)));
-
- jsPlumb_instance.draggable(el, <DragOptions>{ containment: "jsplumb" });
- if(node_list[i].name != "input") {
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isTarget: true,
- anchor: "Top",
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- });
- }
- if(node_list[i].name != "output") {
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isSource: true,
- anchor: "Bottom",
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- });
- }
- }
- };
-
- function drawLines(): void {
- for(let i = 0; i < line_list.length; ++i) {
- let start: Element = document.getElementById(<string>(<unknown>line_list[i][0]));
- let end: Element = document.getElementById(<string>(<unknown>line_list[i][1]));
-
- jsPlumb_instance.connect(
- {
- source: start,
- target: end,
- overlays: [["Arrow", { width: 12, length: 12, location: 0.5 }]],
- },
- connect_common
- );
- }
- };
-
- </script>
-
- <style scoped>
- .jsplumb {
- position: absolute,
- margin: 10px;
- left: 45%;
- width: 50%;
- height: 800px;
- box-sizing: bording-box;
- border: 1px solid black;
- }
- .nodes {
- position: absolute;
- min-width: 60px;
- height: 40px;
- color: black;
- border: 1px solid black;
- line-height: 40px;
- }
- </style>
D3 的全称是 Data-Driven Document,可以理解为:由数据来决定绘图流程的程序设计模型。D3 是一个JavaScript的函数库,是用来做数据可视化的。将数据变成图形,要想用原生的 HTML、SVG、Canvas 来实现是烦琐和困难的。D3 为我们封装好这些,让开发者能更注重图表的布局和逻辑。
JavaScript 的前端可视化库,除了 D3 外还有不少:Highcharts、Echarts、Chart.js。它们可以看作一类的,共同特点是封装层次很高,能够非常简单地制作图表,但是给予开发者控制和设计的空间很少。封装层次太高自然会失去部分自由,但太低又会使程序过长,D3 在这一点上取得了平衡。
直接使用以下命令。
npm install d3
导入
import * as D3 from 'd3';
但是如果你用了ts语言的话会报错Could not find a declaration file for module 'd3',因为d3是js写的。解决方法很简单,找到vue3工程下的shims-vue.d.ts文件,添加以下内容。
declare module 'd3'
咱们想要使用d3,就必须遵循d3对数据格式的要求,这里我们会使用树状图,因此需要把原始数据转换成树的形式。
顺便提一下为什么选择树状图,我想要绘制的图其实也不是完全的树状结构,而是更接近d3的网络结构图。因为树状图的子节点只会存在一个父节点,但是我希望子节点可以存在多个父节点,而网络结构图可以满足这个要求。那为啥不选网络结构图呢,因为我希望生成的图形层级结构更加清晰,这点是树状图的优势,所以关键就是怎么解决树状图不能存在多个父节点的问题了。不过还有一点需要提一下,树状图有一个缺点是不能存在多个输入节点,大家如果由这个需求的话,可能要考虑考虑用其他的图。
首先,我们先不管这个问题哈,我们先要知道d3的树状图需要什么样的输入,我已经展示在下方了。注意这里要遵循一个原则,即使子节点存在于多个父节点下,也不能在树中存在等多个相同的子节点,否则你会发现绘制的图上有重复的节点,所以我们先根据原始数据的in_id与id的关系来生成树,当子节点存在多个父节点时,将子节点插在id较小的父节点下。最后得到的树应该就是像下面这样。
- let blocks_tree: Object = {
- id: -1;
- name: "input",
- children: [
- {
- id: 0,
- name: "block",
- value: ["3"],
- children: [
- {
- id: 1,
- name: "block-long",
- value: ["32"],
- children: [
- {
- id: 2,
- name: "block",
- value: ["64"]
- children: [
- {
- id: 3,
- name: "block-long",
- value: ["64"],
- },
- {
- id: 6,
- name: "block",
- value: ["256"],
- children: [
- {
- id: 7,
- name: "block-long",
- value: ["256"],
- children: [
- {
- id: 9,
- name: "output",
- },
- ],
- },
- {
- id: 8,
- name: "output",
- }
- ],
- },
- ],
- },
- {
- id: 4,
- name: "block",
- value: ["128"],
- children: [
- id: 5,
- name: "block-long",
- value: ["128"],
- ],
- },
- ],
- },
- ],
- },
- ],
- };
但是我们的原始数据格式是不变的,大家可以回到上面去看原始数据的格式,因此需要一个格式转换的算法,如下,细节说明已经在代码中标注了。
- // 创建一个树的接口,必须是嵌套的
- export interface BlockTree {
- id: number;
- name: string;
- value?: string[];
- children?: BlockTree[];
- }
-
- let blocks_tree: BlockTree = { id: -100, name: "unknown" }; // 初始化
-
- function transform(): void {
- // 生成block_list,包含所有Block和input\output
- let block_list: PlumbBlock[] = [];
-
- block_list.push({
- id: input,
- name: "input",
- });
-
- for(let i = 0; i < blocks.length; ++i) {
- block_list.push({
- id: blocks[i].id,
- in_id: blocks[i].in_id,
- name: blocks[i].name,
- value: blocks[i].value,
- });
- }
-
- for(let i = 0; i < output.length; ++i) {
- let id = blocks.length + i;
- block_list.push({
- id: id,
- in_id: [output[i]],
- name: "output",
- });
- }
-
- // 生成树,根据block_list的in_id<->id的关系插入,当一个子级存在多个父级时,将子级插在id较小的父级下
- blocks_tree = { id: input, name: "input", children: [] }; // 初始化时直接把input插入
- for(let i = 0; i < block_list.length; ++i) {
- let in_id = block_list[i].in_id;
- let min: number = in_id[0]; // 除了input外都是存在in_id的,且循环不包含input
-
- for(let j = 1; j < in_id.length; ++j) {
- if(in_id[j] < min)
- min = in_id[j];
- }
- addChildren(blocks_tree, min, block_list[i]);
- }
- }
-
- function addChildren(tree: BlockTree, idx: number, block: PlumbBlock): void {
- let key: keyof BlockTree;
- let find: boolean = false;
- for(key in tree) {
- if(key == "id") {
- if(tree[key] != idx)
- break; // id不对,直接不用继续比较了
- else
- find = true; // 说明找到叶子了
- }
- }
-
- if(!find) {
- if('children' in tree)
- for(let i = 0; i < tree.children.length; ++i)
- addChildren(tree.children[i], idx, block);
- }
- else { // 找到叶子后把新的block插在叶子的children属性中
- // 确保有children属性
- if(!('children' in tree))
- tree.children = [];
-
- if('value' in block)
- tree.children.push({
- id: block.id,
- name: block.name,
- value: block.value,
- });
- else
- tree.children.push({
- id: block.id,
- name: block.name,
- });
- }
- }
得到了树结构数据后,就可以由d3来生成树状图了。
这里就是解决之前那个问题的关键了,d3的树状图本来可以直接生成连接线的,使用以下函数。
- let links = treeData.links();
-
- lineList = links.map(item => {
- return {
- source: item.source.data.id,
- target: item.target.data.id,
- }
- })
而我这里放弃使用这个函数,而用原先的addEndpoints以及drawLines两个函数替代,这样就可以自由的生成连接线了。
- // 修改node的生成方式,使用d3的树状图生成
- function setNodeInfo(): void {
- let data = D3.hierachy(blocks_tree);
-
- let style = window.getcomputedStyle(document.getElementById('jsplumb')); // 获取元素的风格
- let canvas_width: number = Number(style.width.split('px')[0]);
- let canvas_height: number = Number(style.height.split('px')[0]);
-
- // 限制元素的位置
- let scale_width: number = 0.9;
- let scale_height: number = 0.9;
-
- // 创建树,根据jsplumb元素的尺寸来限制树的尺寸,这个size会和nodesize属性冲突
- let treeGenerator = D3.tree().size([canvas_width * scale_width, canvas_height * scale_height]);
- let treeData = treeGenerator(data);
-
- let nodes = treeData.descendants();
-
- node_list.value = nodes.map((item) => {
- return {
- id: item.data.id,
- name: item.data.name,
- left: item.x + "px",
- top: item.y + 20+ "px",
- };
- });
-
- for(let i = 0; i < blocks.length; ++i) {
- let in_id: number[] | undefined = blocks[i].in_id;
- if(in_id != undefined) {
- for(let j = 0; j < in_id.length; ++j)
- line_list.push([in_id[j], blocks[i].id);
- }
- };
-
- for(let i = 0; i < output.length; ++i)
- line_list.push([output[i], id);
- };
1、由d3生成树状图包括了生成node,这个node的left和top都是由d3得到的,因此都要设置成变量,所以template和接口中也要做相应的改变。
:style="{left: item.left, top: item.top}"
- export interface PlumbBlock {
- id: number;
- name: string;
- in_id?: number[] | undefined;
- value?: string[];
- left?: string; // 用来控制元素的水平位置
- top?: string;
- };
2、为了方便控制生成的内容style,直接将common设置成jsplumb的全局属性了,但是要注意所有属性是首字母大写的
- let common: Object = {
- Connector: ["Bezier", { curviness: 15 }],
- Overlays: [["Arrow", { width: 12, length: 12, location: 0.5 }]],
- Anchor: ["AutoDefault"], // 这是由于自动布局的话,难以保证层级关系非常合理,容易产生输入输出的连接线出现在同一端点上的情况,使用autodefault可以看上去更加合理
- //anchor: ["Top", "Bottom"],
-
- Endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- EndpointStyle: {
- stroke: "black",
- fill: "white",
- strokeWidth: 2,
- },
- };
3、函数的位置也很关键,我在下方标注了。
- onBeforeonMount(() => {
- transform(); // 用来生成树
- });
-
- onMounted(() => {
- setNodeInfo(); // 移到这里是因为为了根据页面尺寸自适应的生成树,因此需要等jsplumb挂载完
- jsPlumb_instance = jsPlumb.getInstance();
-
- jsPlumb_instance.ready(function() {
- nextTick(() => { // 增加nextTick是为了等待树的元素都挂载完,这样addEndpoints中才能给元素添加端点
- addEndpoints();
- drawLines();
- })
- });
- });
效果如下,是不是还不错。
- <template>
- <div id="jsplumb" class="jsplumb">
- <div
- class="nodes"
- v-for="item in node_list"
- :key="item.id"
- :id="item.id"
- :style="{left: item.left, top: item.top}"
- >{{ item.name }}<div>
- </div>
- </template>
-
- <script lang="ts" setup>
- import { jsPlumb } from 'jsplumb';
- import * as D3 from 'd3';
- import type {
- Element,
- DragOptions,
- EndpointOptions,
- EndpointSpec,
- jsPlumbInstance,
- } from 'jsplumb';
- import { ref, onBeforeonMount, onMounted, nextTick } from 'vue';
-
- // 创建一个树的接口,必须是嵌套的
- export interface BlockTree {
- id: number;
- name: string;
- value?: string[];
- children?: BlockTree[];
- }
-
- export interface PlumbBlock {
- id: number;
- name: string;
- in_id?: number[] | undefined;
- value?: string[];
- left?: string; // 用来控制元素的水平位置
- top?: string;
- };
-
- let jsPlumb_instance: jsPlumbInstance = null;
- let blocks_tree: BlockTree = { id: -100, name: "unknown" }; // 初始化
- let node_list= ref<PlumbBlock>([]); // 需要改成ref的
- let line_list: number[][] = [];
-
- onBeforeonMount(() => {
- transform(); // 用来生成树
- });
-
- onMounted(() => {
- setNodeInfo(); // 移到这里是因为为了根据页面尺寸自适应的生成树,因此需要等jsplumb挂载完
- jsPlumb_instance = jsPlumb.getInstance();
- jsPlumb_instance.importDefaults(common);
-
- jsPlumb_instance.ready(function() {
- nextTick(() => { // 增加nextTick是为了等待树的元素都挂载完,这样addEndpoints中才能给元素添加端点
- addEndpoints();
- drawLines();
- })
- });
- });
-
- // 直接设置成jsplumb的全局属性了,但是要注意所有属性是首字母大写的
- let common: Object = {
- Connector: ["Bezier", { curviness: 15 }],
- Overlays: [["Arrow", { width: 12, length: 12, location: 0.5 }]],
- Anchor: ["AutoDefault"], // 这是由于自动布局的话,难以保证层级关系非常合理,容易产生输入输出的连接线出现在同一端点上的情况,使用autodefault可以看上去更加合理
- //Anchor: ["Top", "Bottom"],
-
- Endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- EndpointStyle: {
- stroke: "black",
- fill: "white",
- strokeWidth: 2,
- },
- };
- let input: number = -1;
- let output: number[] = [6, 7];
- let blocks: PlumbBlock[] = [
- {
- id: 0,
- in_id: [-1],
- name: "block",
- value: ["3"]
- },
- {
- id: 1,
- in_id: [0],
- name: "block-long",
- value: ["32"]
- },
- {
- id: 2,
- in_id: [1],
- name: "block",
- value: ["64"]
- },
- {
- id: 3,
- in_id: [2],
- name: "block-long",
- value: ["64"]
- },
- {
- id: 4,
- in_id: [1, 3],
- name: "block",
- value: ["128"]
- },
- {
- id: 5,
- in_id: [4],
- name: "block-long",
- value: ["128"]
- },
- {
- id: 6,
- in_id: [2, 5],
- name: "block",
- value: ["256"]
- },
- {
- id: 7,
- in_id: [6],
- name: "block-long",
- value: ["256"]
- }
- ];
-
- function transform(): void {
- // 生成block_list,包含所有Block和input\output
- let block_list: PlumbBlock[] = [];
-
- block_list.push({
- id: input,
- name: "input",
- });
-
- for(let i = 0; i < blocks.length; ++i) {
- block_list.push({
- id: blocks[i].id,
- in_id: blocks[i].in_id,
- name: blocks[i].name,
- value: blocks[i].value,
- });
- }
-
- for(let i = 0; i < output.length; ++i) {
- let id = blocks.length + i;
- block_list.push({
- id: id,
- in_id: [output[i]],
- name: "output",
- });
- }
-
- // 生成树,根据block_list的in_id<->id的关系插入,当一个子级存在多个父级时,将子级插在id较小的父级下
- blocks_tree = { id: input, name: "input", children: [] }; // 初始化时直接把input插入
- for(let i = 0; i < block_list.length; ++i) {
- let in_id = block_list[i].in_id;
- let min: number = in_id[0]; // 除了input外都是存在in_id的,且循环不包含input
-
- for(let j = 1; j < in_id.length; ++j) {
- if(in_id[j] < min)
- min = in_id[j];
- }
- addChildren(blocks_tree, min, block_list[i]);
- }
- }
-
- function addChildren(tree: BlockTree, idx: number, block: PlumbBlock): void {
- let key: keyof BlockTree;
- let find: boolean = false;
- for(key in tree) {
- if(key == "id") {
- if(tree[key] != idx)
- break; // id不对,直接不用继续比较了
- else
- find = true; // 说明找到叶子了
- }
- }
-
- if(!find) {
- if('children' in tree)
- for(let i = 0; i < tree.children.length; ++i)
- addChildren(tree.children[i], idx, block);
- }
- else { // 找到叶子后把新的block插在叶子的children属性中
- // 确保有children属性
- if(!('children' in tree))
- tree.children = [];
-
- if('value' in block)
- tree.children.push({
- id: block.id,
- name: block.name,
- value: block.value,
- });
- else
- tree.children.push({
- id: block.id,
- name: block.name,
- });
- }
- }
-
-
- // 修改node的生成方式,使用d3的树状图生成
- function setNodeInfo(): void {
- let data = D3.hierachy(blocks_tree);
-
- let style = window.getcomputedStyle(document.getElementById('jsplumb')); // 获取元素的风格
- let canvas_width: number = Number(style.width.split('px')[0]);
- let canvas_height: number = Number(style.height.split('px')[0]);
-
- // 限制元素的位置
- let scale_width: number = 0.9;
- let scale_height: number = 0.9;
-
- // 创建树,根据jsplumb元素的尺寸来限制树的尺寸,这个size会和nodesize属性冲突
- let treeGenerator = D3.tree().size([canvas_width * scale_width, canvas_height * scale_height]);
- let treeData = treeGenerator(data);
-
- let nodes = treeData.descendants();
-
- node_list.value = nodes.map((item) => {
- return {
- id: item.data.id,
- name: item.data.name,
- left: item.x + "px",
- top: item.y + 20+ "px",
- };
- });
-
- for(let i = 0; i < blocks.length; ++i) {
- let in_id: number[] | undefined = blocks[i].in_id;
- if(in_id != undefined) {
- for(let j = 0; j < in_id.length; ++j)
- line_list.push([in_id[j], blocks[i].id);
- }
- };
-
- for(let i = 0; i < output.length; ++i)
- line_list.push([output[i], id);
- };
-
- function addEndpoints(): void {
- for(let i = 0; i < node_list.value.length; ++i) {
- let el = <Element>(document.getElementById(String(node_list.value[i].id)));
-
- jsPlumb_instance.draggable(el, <DragOptions>{ containment: "jsplumb" });
- if(node_list.value[i].name != "input") {
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isTarget: true,
- anchor: "Top",
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- });
- }
- if(node_list.value[i].name != "output") {
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isSource: true,
- anchor: "Bottom",
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- });
- }
- }
- };
-
- function drawLines(): void {
- for(let i = 0; i < line_list.length; ++i) {
- let start: Element = document.getElementById(String(line_list[i][0]));
- let end: Element = document.getElementById(String(line_list[i][1]));
-
- jsPlumb_instance.connect({
- source: start,
- target: end,
- });
- }
- };
-
- </script>
-
- <style scoped>
- .jsplumb {
- position: absolute,
- margin: 10px;
- left: 45%;
- width: 50%;
- height: 800px;
- box-sizing: bording-box;
- border: 1px solid black;
- }
- .nodes {
- position: absolute;
- min-width: 60px;
- height: 40px;
- color: black;
- border: 1px solid black;
- line-height: 40px;
- padding: 0 10px;
- }
- </style>
为了便于存储和更新数据,也为了让逻辑更加清晰,先对数据格式做了以下修改:
1、拆分PlumbBlock接口,PlumbBlock为存储原始数据的最小单元,PlumbNode为画布显示的最小单元
- interface PlumbBlock {
- id: number;
- name: string;
- in_id?: number[] | undefined;
- value?: string[];
- };
-
- interface PlumbNode {
- id: number;
- name: string;
- left: string;
- top: string;
- }
2、将input、output、blocks合并成一个block_list
- let block_list: PlumbBlock = [];
- block_list = [
- {
- id: -1,
- name: "input",
- },
- {
- id: 0,
- in_id: [-1],
- name: "block",
- value: ["3"]
- },
- {
- id: 1,
- in_id: [0],
- name: "block-long",
- value: ["32"]
- },
- {
- id: 2,
- in_id: [1],
- name: "block",
- value: ["64"]
- },
- {
- id: 3,
- in_id: [2],
- name: "block-long",
- value: ["64"]
- },
- {
- id: 4,
- in_id: [1, 3],
- name: "block",
- value: ["128"]
- },
- {
- id: 5,
- in_id: [4],
- name: "block-long",
- value: ["128"]
- },
- {
- id: 6,
- in_id: [2, 5],
- name: "block",
- value: ["256"]
- },
- {
- id: 7,
- in_id: [6],
- name: "block-long",
- value: ["256"]
- },
- {
- id: 8,
- in_id: [6],
- name: "output"
- },
- {
- id: 9,
- in_id: [7],
- name: "output"
- }
- ];
修改jsplumb默认配置、template和css,实现鼠标悬停可以高亮节点和连线。
- <template>
- <div id="jsplumb" class="jsplumb">
- <div
- class="nodes"
- v-for="item in node_list"
- :key="item.id"
- :id="item.id"
- @mouseleave="mouseleave(item.id)"
- :style="{left: item.left, top: item.top}"
- >{{ item.name }}<div>
- </div>
- </template>
-
- <script lang="ts" setup>
- // 直接设置成jsplumb的全局属性了,但是要注意所有属性是首字母大写的
- let common: Object = {
- Overlays: [["Arrow", { width: 12, length: 12, location: 0.5 }]],
- Anchor: ["Top", "Bottom"],
-
- MaxConnections: -1, // 每个端点可以连接多条线
- Connector: ["Bezier", { curviness: 15 }],
- PaintStyle: { // 聚焦前的style
- stroke: "#99ccff",
- strokeWidth: 3,
- },
- HoverPaintStyle: { // 聚焦后的style
- stroke: "#0077cc",
- strokeWidth: 4,
- },
-
- Endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- EndpointStyle: { // 聚焦前的style
- stroke: "#77ccff",
- fill: "#ffffff",
- strokeWidth: 2,
- },
- EndpointHoverStyle: { // 聚焦后的style
- stroke: "#0077cc",
- fill: "#0077cc",
- strokeWidth: 2,
- },
- };
-
- // 移动节点后需要在鼠标离开后更新当前位置
- function mouseleave(id: number): void {
- let style = window.getcomputedStyle(document.getElementById(String(id)));
- for(let i = 0; i < node_list,value.length; ++i) {
- if(node_list.value[i].id === id) {
- node_list.value[i].left = style.left;
- node_list.vlaue[i].top = style.top;
- }
- }
- }
- </script>
-
- <style scoped>
- .jsplumb {
- position: absolute,
- margin: 10px;
- left: 45%;
- width: 50%;
- height: 800px;
- box-sizing: bording-box;
- border: 1px solid black;
- }
- .nodes {
- position: absolute;
- min-width: 60px;
- height: 40px;
- color: black;
- border: 1px solid black;
- background-color: #eeeeee;
- line-height: 40px;
- padding: 0 10px;
- }
- .nodes:hover{
- position: absolute;
- min-width: 60px;
- height: 40px;
- color: black;
- border: 1px solid black;
- background-color: #eeeeaa;
- line-height: 40px;
- padding: 0 10px;
- }
- </style>
正常界面。
鼠标悬停在节点上。
鼠标悬停在连接线上。
双击节点或连接线,可以弹出操作菜单。
- <template>
- <div id="jsplumb" class="jsplumb">
- <div
- class="menu"
- v-if="show"
- :style="{
- left: menu_left,
- top: menu_top,
- }"
- >
- <el-button class="button" @click="remove" type="danger">删除</el-button>
- <el-button class="button" @click="cancel" type="primary">取消</el-button>
- </div>
- </div>
- </template>
-
- <script lang="ts" setup>
- let show = ref<boolean>(false); // 控制菜单的显示
- let menu_left = ref<string>(""); // 控制菜单的水平位置
- let menu_top = ref<string>(""); // 控制菜单的垂直位置
-
- // 节点双击弹出菜单,
- function dblclick(id: number): void {
- let style = window.getcomputedStyle(document.getElementById(String(id))); // 获取dom元素信息
- let x: string = Number(style.left.split('px')[0]) + Number(style.width.split('px')[0]) + "px"; // 想定位精确一点的话还应该加上padding、border等参数,这些都可以在style中获取
- let y: string = Number(style.top.split('px')[0]) + 'px';
-
- menu_left.value = x;
- menu_top.value = y;
- show.value = true;
- cur_type = DomType.node;
- cur_source_id = String(id);
- }
-
- // 连接线事件都在这儿
- function bindEvent(): void {
- // 双击弹出菜单
- jsPlumb_instance.bind("dblclick", function(info) {
- let style = window.getcomputedStyle(document.getElementById(info.sourceId));
- let x_1: string = Number(style.left.split('px')[0]) + Number(style.width.split('px')[0];
- let y_1: string = Number(style.top.split('px')[0]) + Number(style.height.split('px')[0]);
- style = window.getcomputedStyle(document.getElementById(info.targetIdId));
- let x_2: string = Number(style.left.split('px')[0]) + Number(style.width.split('px')[0];
- let y_2: string = Number(style.top.split('px')[0]);
-
- let x: string = (x_1 + x_2) / 2 + 'px';
- let y: string = (y_1 + y_2) / 2 + 'px';
-
- menu_left.value = x;
- menu_top.value = y;
- show.value = true;
- cur_type = DomType.connection;
- cur_source_id = info.sourceId;
- cur_source_id = info.targetId;
- });
- }
- </script>
-
- <style scoped>
- .menu {
- position:absolute;
- }
- </style>
双击节点
双击连接线
实现节点或连接线的添加和删除。
- // 用于区分Dom元素的类型,做逻辑判断用
- enum DomType {
- empty,
- node,
- connection
- }
-
- let cur_type: DomType = DomType.empty;
- let cur_source_id: string = "";
- let cur_target_id: string = "";
-
- function remove(): void {
- if(cur_type === DomType.node && cur_source_id !== "") {
- let find: boolean = false;
- for (let i = 1; i < block_list.length; ++i) {
- if(String(block_list[i].id) === cur_source_id) {
- block_list.splice(i, 1);
- find = true;
- break;
- }
- }
- // 删除节点及其连接线
- jsPlumb_instance.remove(cur_source_id);
- }
- else if(cur_type === DomType.connect && cur_source_id !== "" && cur_target_id !== "") {
- let connections = jsPlumb_instance.getAllConnections(); // 找到所有连接
- for(let idx in connections) {
- if(connections[idx].sourceId === cur_source_id && connections[idx].targetId === cur_target_id) { // 筛选出首尾相同的连接并删除
- jsPlumb_instance.deleteConnection(connections[idx]);
- break;
- }
- }
- }
- show.value = false;
- cur_type = DomType.empty;
- }
-
- function cancel(): void {
- show.value = false;
- cur_type = DomType.empty;
- }
-
- // 连接线事件都在这儿
- function bindEvent(): void {
- // 连接节点,删除重复连接,并确保output节点只有一个输入
- jsPlumb_instance.bind("connection", function(info) {
- // target是output
- for (let i = 1; i < block_list.length; ++i) {
- if(String(block_list[i].id) === info.targetId && block_list[i].name === "output" && block_list[i].in_id.length > 0) {
- let arr = jsPlumb_instance.select({ target: info.targetId });
- if(arr.length > 1) {
- block_list.splice(i, 1);
- break;
- }
- }
- }
- // target不是output
- let arr = jsPlumb_instance.select({ source: info.sourceId, target: info.targetId });
- if(arr.length > 1)
- jsPlumb_instance.deleteConnection(info.connection);
-
- need_update.value = true;
- });
-
- // 删除连接线,更新数据
- jsPlumb_instance.bind("connectionDetached", function(info) {
- need_update.value = true;
- });
- }
添加连接线。
删除节点。
获取画布上的连接信息,更新本地数据。
- let need_update = ref<boolean>(false); // 由watch监视,决定是否更新数据
-
- // 连接线事件都在这儿
- function bindEvent(): void {
- // 连接节点,删除重复连接,并确保output节点只有一个输入
- jsPlumb_instance.bind("connection", function(info) {
- // ...
- need_update.value = true;
- });
-
- // 删除连接线,更新数据
- jsPlumb_instance.bind("connectionDetached", function(info) {
- need_update.value = true;
- });
- }
-
- // 更新block_list中的连接关系
- function update(): void {
- let connections = jsPlumb_instance.getAllConnections();
- // 先清空
- for(let i = 1; i < block_list.length; ++i)
- block_list[i].in_id.splice(0, block_list[i].in_id.length);
-
- // 插入新的连接线
- for(let i = 0; i < connections.length; ++i) {
- for(let j = 1; j < block_list.length; ++j) {
- if(String(block_list[j].id) === connection[i].targetId)
- block_list[j].in_id.push(Number(connections[i].sourceId));
- }
- }
- need_update.value = false;
- }
-
- // 监视need_update
- watch(
- () => need_update.value,
- (cur: boolean, pre: boolean) => {
- if(cur === true)
- update();
- }
- );
删除一个节点后更新数据,可以看到id为1的节点和相关的in_id没有了。
- <template>
- <div id="jsplumb" class="jsplumb">
- <div
- class="nodes"
- v-for="item in node_list"
- :key="item.id"
- :id="item.id"
- @mouseleave="mouseleave(item.id)"
- @dblclick="dblclick(item.id)"
- :style="{left: item.left, top: item.top}"
- >{{ item.name }}<div>
- <!--增加两个交互操作-->
- <div
- class="menu"
- v-if="show"
- :style="{
- left: menu_left,
- top: menu_top,
- }"
- >
- <el-button class="button" @click="remove" type="danger">删除</el-button>
- <el-button class="button" @click="cancel" type="primary">取消</el-button>
- </div>
- <!--增加右击弹出的菜单-->
- </div>
- </template>
-
- <script lang="ts" setup>
- import { jsPlumb } from 'jsplumb';
- import * as D3 from 'd3';
- import type {
- Element,
- DragOptions,
- EndpointOptions,
- EndpointSpec,
- jsPlumbInstance,
- Connection,
- } from 'jsplumb';
- import { ref, onBeforeonMount, onMounted, nextTick, watch } from 'vue';
-
- // 创建一个树的接口,必须是嵌套的
- interface BlockTree {
- id: number;
- name: string;
- value?: string[];
- children?: BlockTree[];
- }
-
- // 拆分接口,逻辑更清晰,PlumbBlock为存储原始数据的最小单元,PlumbNode为画布显示的最小单元
- interface PlumbBlock {
- id: number;
- name: string;
- in_id?: number[] | undefined;
- value?: string[];
- };
-
- interface PlumbNode {
- id: number;
- name: string;
- left: string;
- top: string;
- }
-
- // 用于区分Dom元素的类型,做逻辑判断用
- enum DomType {
- empty,
- node,
- connection
- }
-
- let jsPlumb_instance: jsPlumbInstance = null;
- let block_tree: BlockTree = { id: -100, name: "unknown" }; // 初始化
- let block_list: PlumbBlock = [];
- let node_list = ref<PlumbNode>([]);
- let line_list: number[][] = [];
- let show = ref<boolean>(false); // 控制菜单的显示
- let menu_left = ref<string>(""); // 控制菜单的水平位置
- let menu_top = ref<string>(""); // 控制菜单的垂直位置
- let cur_type: DomType = DomType.empty;
- let cur_source_id: string = "";
- let cur_target_id: string = "";
- let need_update = ref<boolean>(false); // 由watch监视,决定是否更新数据
-
-
- onBeforeonMount(() => {
- transform(); // 用来生成树
- });
-
- onMounted(() => {
- setNodeInfo(); // 移到这里是因为为了根据页面尺寸自适应的生成树,因此需要等jsplumb挂载完
- jsPlumb_instance = jsPlumb.getInstance();
- jsPlumb_instance.importDefaults(common);
-
- jsPlumb_instance.ready(function() {
- nextTick(() => { // 增加nextTick是为了等待树的元素都挂载完,这样addEndpoints中才能给元素添加端点
- addEndpoints();
- drawLines();
- bindEvent();
- })
- });
- });
-
- // 直接设置成jsplumb的全局属性了,但是要注意所有属性是首字母大写的
- let common: Object = {
- Overlays: [["Arrow", { width: 12, length: 12, location: 0.5 }]],
- Anchor: ["Top", "Bottom"],
-
- MaxConnections: -1, // 每个端点可以连接多条线
- Connector: ["Bezier", { curviness: 15 }],
- PaintStyle: { // 聚焦前的style
- stroke: "#99ccff",
- strokeWidth: 3,
- },
- HoverPaintStyle: { // 聚焦后的style
- stroke: "#0077cc",
- strokeWidth: 4,
- },
-
- Endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- EndpointStyle: { // 聚焦前的style
- stroke: "#77ccff",
- fill: "#ffffff",
- strokeWidth: 2,
- },
- EndpointHoverStyle: { // 聚焦后的style
- stroke: "#0077cc",
- fill: "#0077cc",
- strokeWidth: 2,
- },
- };
-
- block_list = [
- {
- id: -1,
- name: "input",
- },
- {
- id: 0,
- in_id: [-1],
- name: "block",
- value: ["3"]
- },
- {
- id: 1,
- in_id: [0],
- name: "block-long",
- value: ["32"]
- },
- {
- id: 2,
- in_id: [1],
- name: "block",
- value: ["64"]
- },
- {
- id: 3,
- in_id: [2],
- name: "block-long",
- value: ["64"]
- },
- {
- id: 4,
- in_id: [1, 3],
- name: "block",
- value: ["128"]
- },
- {
- id: 5,
- in_id: [4],
- name: "block-long",
- value: ["128"]
- },
- {
- id: 6,
- in_id: [2, 5],
- name: "block",
- value: ["256"]
- },
- {
- id: 7,
- in_id: [6],
- name: "block-long",
- value: ["256"]
- },
- {
- id: 8,
- in_id: [6],
- name: "output"
- },
- {
- id: 9,
- in_id: [7],
- name: "output"
- }
- ];
-
- function transform(): void {
- // 生成树,根据block_list的in_id<->id的关系插入,当一个子级存在多个父级时,将子级插在id较小的父级下
- block_tree = { id: input, name: "input", children: [] }; // 初始化时直接把input插入
- for(let i = 1; i < block_list.length; ++i) {
- let in_id = block_list[i].in_id;
- let min: number = in_id[0]; // 除了input外都是存在in_id的,且循环不包含input
-
- for(let j = 1; j < in_id.length; ++j) {
- if(in_id[j] < min)
- min = in_id[j];
- }
- addChildren(blocks_tree, min, block_list[i]);
- }
- }
-
- function addChildren(tree: BlockTree, idx: number, block: PlumbBlock): void {
- let key: keyof BlockTree;
- let find: boolean = false;
- for(key in tree) {
- if(key == "id") {
- if(tree[key] != idx)
- break; // id不对,直接不用继续比较了
- else
- find = true; // 说明找到叶子了
- }
- }
-
- if(!find) {
- if('children' in tree)
- for(let i = 0; i < tree.children.length; ++i)
- addChildren(tree.children[i], idx, block);
- }
- else { // 找到叶子后把新的block插在叶子的children属性中
- // 确保有children属性
- if(!('children' in tree))
- tree.children = [];
-
- if('value' in block)
- tree.children.push({
- id: block.id,
- name: block.name,
- value: block.value,
- });
- else
- tree.children.push({
- id: block.id,
- name: block.name,
- });
- }
- }
-
-
- // 修改node的生成方式,使用d3的树状图生成
- function setNodeInfo(): void {
- let data = D3.hierachy(blocks_tree);
-
- let style = window.getcomputedStyle(document.getElementById('jsplumb'), null); // 获取元素的风格
- let canvas_width: number = Number(style.width.split('px')[0]);
- let canvas_height: number = Number(style.height.split('px')[0]);
-
- // 限制元素的位置
- let scale_width: number = 0.9;
- let scale_height: number = 0.9;
-
- // 创建树,根据jsplumb元素的尺寸来限制树的尺寸,这个size会和nodesize属性冲突
- let treeGenerator = D3.tree().size([canvas_width * scale_width, canvas_height * scale_height]);
- let treeData = treeGenerator(data);
-
- let nodes = treeData.descendants();
-
- node_list.value = nodes.map((item) => {
- return {
- id: item.data.id,
- name: item.data.name,
- left: item.x + "px",
- top: item.y + 20+ "px",
- };
- });
-
- for(let i = 0; i < blocks_list.length; ++i) {
- let in_id: number[] | undefined = blocks_list[i].in_id;
- if(in_id != undefined) {
- for(let j = 0; j < in_id.length; ++j)
- line_list.push([in_id[j], blocks_list[i].id);
- }
- };
- };
-
- function addEndpoints(): void {
- for(let i = 0; i < node_list.value.length; ++i) {
- let el = <Element>(document.getElementById(String(node_list.value[i].id)));
-
- jsPlumb_instance.draggable(el, <DragOptions>{ containment: "jsplumb" });
- if(node_list.value[i].name != "input") {
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isTarget: true,
- anchor: "Top",
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- });
- }
- if(node_list.value[i].name != "output") {
- jsPlumb_instance.addEndpoint(el, <EndpointOptions>{
- isSource: true,
- anchor: "Bottom",
- endpoint: <EndpointSpec>["Dot", { radius: 3 }],
- });
- }
- }
- };
-
- function drawLines(): void {
- for(let i = 0; i < line_list.length; ++i) {
- let start: Element = document.getElementById(String(line_list[i][0]));
- let end: Element = document.getElementById(String(line_list[i][1]));
-
- jsPlumb_instance.connect({
- source: start,
- target: end,
- });
- }
- };
-
- function remove(): void {
- if(cur_type === DomType.node && cur_source_id !== "") {
- let find: boolean = false;
- for (let i = 1; i < block_list.length; ++i) {
- if(String(block_list[i].id) === cur_source_id) {
- block_list.splice(i, 1);
- find = true;
- break;
- }
- }
- // 删除节点及其连接线
- jsPlumb_instance.remove(cur_source_id);
- }
- else if(cur_type === DomType.connect && cur_source_id !== "" && cur_target_id !== "") {
- let connections = jsPlumb_instance.getAllConnections(); // 找到所有连接
- for(let idx in connections) {
- if(connections[idx].sourceId === cur_source_id && connections[idx].targetId === cur_target_id) { // 筛选出首尾相同的连接并删除
- jsPlumb_instance.deleteConnection(connections[idx]);
- break;
- }
- }
- }
- show.value = false;
- cur_type = DomType.empty;
- }
-
- function cancel(): void {
- show.value = false;
- cur_type = DomType.empty;
- }
-
- // 移动节点后需要在鼠标离开后更新当前位置
- function mouseleave(id: number): void {
- let style = window.getcomputedStyle(document.getElementById(String(id)));
- for(let i = 0; i < node_list,value.length; ++i) {
- if(node_list.value[i].id === id) {
- node_list.value[i].left = style.left;
- node_list.vlaue[i].top = style.top;
- }
- }
- }
-
- // 节点双击弹出菜单,
- function dblclick(id: number): void {
- let style = window.getcomputedStyle(document.getElementById(String(id))); // 获取dom元素信息
- let x: string = Number(style.left.split('px')[0]) + Number(style.width.split('px')[0]) + "px"; // 想定位精确一点的话还应该加上padding、border等参数,这些都可以在style中获取
- let y: string = Number(style.top.split('px')[0]) + 'px';
-
- menu_left.value = x;
- menu_top.value = y;
- show.value = true;
- cur_type = DomType.node;
- cur_source_id = String(id);
- }
-
- // 连接线事件都在这儿
- function bindEvent(): void {
- // 双击弹出菜单
- jsPlumb_instance.bind("dblclick", function(info) {
- let style = window.getcomputedStyle(document.getElementById(info.sourceId));
- let x_1: string = Number(style.left.split('px')[0]) + Number(style.width.split('px')[0];
- let y_1: string = Number(style.top.split('px')[0]) + Number(style.height.split('px')[0]);
- style = window.getcomputedStyle(document.getElementById(info.targetIdId));
- let x_2: string = Number(style.left.split('px')[0]) + Number(style.width.split('px')[0];
- let y_2: string = Number(style.top.split('px')[0]);
-
- let x: string = (x_1 + x_2) / 2 + 'px';
- let y: string = (y_1 + y_2) / 2 + 'px';
-
- menu_left.value = x;
- menu_top.value = y;
- show.value = true;
- cur_type = DomType.connection;
- cur_source_id = info.sourceId;
- cur_source_id = info.targetId;
- });
-
- // 连接节点,删除重复连接,并确保output节点只有一个输入
- jsPlumb_instance.bind("connection", function(info) {
- // target是output
- for (let i = 1; i < block_list.length; ++i) {
- if(String(block_list[i].id) === info.targetId && block_list[i].name === "output" && block_list[i].in_id.length > 0) {
- let arr = jsPlumb_instance.select({ target: info.targetId });
- if(arr.length > 1) {
- block_list.splice(i, 1);
- break;
- }
- }
- }
- // target不是output
- let arr = jsPlumb_instance.select({ source: info.sourceId, target: info.targetId });
- if(arr.length > 1)
- jsPlumb_instance.deleteConnection(info.connection);
-
- need_update.value = true;
- });
-
- // 删除连接线,更新数据
- jsPlumb_instance.bind("connectionDetached", function(info) {
- need_update.value = true;
- });
- }
-
- // 更新block_list中的连接关系
- function update(): void {
- let connections = jsPlumb_instance.getAllConnections();
- // 先清空
- for(let i = 1; i < block_list.length; ++i)
- block_list[i].in_id.splice(0, block_list[i].in_id.length);
-
- // 插入新的连接线
- for(let i = 0; i < connections.length; ++i) {
- for(let j = 1; j < block_list.length; ++j) {
- if(String(block_list[j].id) === connection[i].targetId)
- block_list[j].in_id.push(Number(connections[i].sourceId));
- }
- }
- need_update.value = false;
- }
-
- // 监视need_update
- watch(
- () => need_update.value,
- (cur: boolean, pre: boolean) => {
- if(cur === true)
- update();
- }
- );
- </script>
-
- <style scoped>
- .jsplumb {
- position: absolute,
- margin: 10px;
- left: 45%;
- width: 50%;
- height: 800px;
- box-sizing: bording-box;
- border: 1px solid black;
- }
- .nodes {
- position: absolute;
- min-width: 60px;
- height: 40px;
- color: black;
- border: 1px solid black;
- background-color: #eeeeee;
- line-height: 40px;
- padding: 0 10px;
- }
- .nodes:hover{
- position: absolute;
- min-width: 60px;
- height: 40px;
- color: black;
- border: 1px solid black;
- background-color: #eeeeaa;
- line-height: 40px;
- padding: 0 10px;
- }
- .menu {
- position:absolute;
- }
- </style>
jsPlumb 基本概念 - 简书 (jianshu.com)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。