这篇文章就给大家展示个人的一个jsplumb成品,也是放在自己的项目之中.注释我基本上也都写好了,但是目前代码还没有进行整理,还有很多的测试痕迹以及备注打印.
先说包含的功能:
1.将节点拖拽到画布,精准放置画布内
2.画布中的节点可以自己主动去连线
3.画布节点和连线点击可以查看详情,并且可以删除节点和连线,并且重新绘制画布.
效果图如下:
文章来源:https://www.toymoban.com/news/detail-759913.html
全部代码:
父组件(左侧和画布组件)
<template>
<ul class="box">
<li class="leftMenu">
<h3>选择节点</h3>
<el-collapse>
<el-collapse-item
:title="item1.name"
v-for="(item1, index) in leftMenuData"
:key="index"
>
<draggable
@start="moveStart"
@end="moveEnd"
v-model="item1.children"
:options="options"
>
<div
v-for="(item2, n) in item1.children"
class="content"
:divOption="JSON.stringify(item2)"
@mousedown="mouseDownFun"
:key="n"
>
{{ item2.config[0].value }}
</div>
</draggable>
</el-collapse-item>
</el-collapse>
</li>
<li class="plumbBox" id="plumbBox">
<div
v-for="(item, index) in info"
:key="index"
:id="item.id"
:style="getStyle(item)"
:class="item.id === activeNode.id ? 'activeNode' : 'normalNode'"
@click="sendActive(item)"
>
<div class="plumbNode" :id="item.id + 'plumbNode'">
<el-icon :size="20">
<CirclePlusFilled />
</el-icon>
</div>
{{ item.config[0].value }}
<el-icon
class="is-loading"
v-if="item.status === 'loading'"
color="blue"
>
<Loading />
</el-icon>
<el-icon v-else-if="item.status === 'success'" color="green">
<CircleCheckFilled />
</el-icon>
<el-icon v-else-if="item.status === 'error'" color="red">
<CircleCloseFilled />
</el-icon>
</div>
</li>
<li class="rightContent">
<h3>节点操作</h3>
<div style="padding-left: 10px">
<RightForm
ref="rightForm"
@changeActiveNodeInfo="changeActiveNodeInfo"
@deleteLine="deleteLine"
@deleteNode="deleteNode"
></RightForm>
</div>
</li>
</ul>
</template>
<script setup>
//引入jsPlumb
import { jsPlumb } from "jsplumb";
import { VueDraggableNext } from "vue-draggable-next";
import { ElMessage } from "element-plus";
import lodash from "lodash";
import { v4 as uuidv4 } from "uuid";
import { reactive } from "vue";
import RightForm from "./rightForm";
const draggable = VueDraggableNext;
let plumbBox = null;
let plumbBoxPositionInfo = reactive({});
//鼠标和节点的内部差距,为了让节点更精准的判断区域
let nodePositionDiff = reactive({});
//后面需要回传给父组件的值
let plumbList = ref([]);
//绘制标识
let renderFlag = ref(undefined);
//动态节点
let activeNode = ref({});
let inputVal = ref("名称1");
let rightForm = ref(null);
/*
----------------------------------------------
//连线基础配置
let jsPlumbConnectOptions = {
isSource: true,
isTarget: true,
// 动态锚点、提供了4个方向 Continuous、AutoDefault
anchor: ["Continuous",{shape:"Circle"}],
overlays: [['Arrow', { width: 8, length: 8, location: 1 }]] // overlay
}
//画布节点的拖拽连线配置
const jsplumbSourceOptions = {
filter:'.plumbNode',
filterExclude:false,
anchor:'Continuous',
allowLoopback:true,
maxConnections: -1,
onMaxConnections:function (info,e){
console.log(`超过了最大值连线:${info.maxConnections}`)
}
}
----------------------------------------------
*/
//左侧菜单节点的拖拽配置
const options = {
preventOnFilter: false,
sort: false,
disabled: false,
ghostClass: "tt",
// 不使用H5原生的配置
forceFallback: true,
};
//默认配置
let globalConfig = {
Container: "plumbBox",
anchor: ["Bottom", "Top", "Left", "Right"],
connector: "Bezier",
endpoint: "Blank",
paintStyle: {
stroke: "#364249",
strokeWidth: 1,
outlineStroke: "transparent",
outlineWidth: 10,
},
hoverPaintStyle: { stroke: "#000", strokeWidth: 1.3 },
overlays: [["Arrow", { width: 5, length: 5, location: 1 }]],
endpointStyle: {
fill: "lightgray",
outlineStroke: "darkgray",
outlineWidth: 2,
},
};
//左侧
let leftMenuData = ref([
{
name: "起始列表",
children: [
{
to: [],
top: 0,
left: 0,
status: "loading",
isSource: true,
isTarget: false,
config: [
{
label: "名称",
name: "label",
type: "text",
value: "起始节点1",
require: true,
},
{
label: "描述",
name: "description",
type: "textarea",
value: "",
require: false,
},
{
label: "归属",
name: "affiliation",
type: "select",
value: "check",
require: true,
options: [
{ label: "审核信息", value: "check" },
{ label: "生产经营", value: "manage" },
{ label: "结算报销", value: "account" },
],
},
],
},
{
to: [],
top: 0,
left: 0,
status: "loading",
isSource: true,
isTarget: true,
config: [
{
label: "名称",
name: "label",
type: "text",
value: "起始节点2",
require: true,
},
{
label: "描述",
name: "description",
type: "textarea",
value: "",
require: false,
},
{
label: "归属",
name: "affiliation",
type: "select",
value: "check",
require: true,
options: [
{ label: "审核信息", value: "check" },
{ label: "生产经营", value: "manage" },
{ label: "结算报销", value: "account" },
],
},
],
},
],
},
{
name: "完结列表",
children: [
{
to: [],
top: 0,
left: 0,
status: "loading",
type: "target",
isSource: false,
isTarget: false,
config: [
{
label: "名称",
name: "label",
type: "text",
value: "完结节点1",
require: true,
},
{
label: "描述",
name: "description",
type: "textarea",
value: "check",
require: false,
},
{
label: "归属",
name: "affiliation",
type: "select",
value: "",
require: true,
options: [
{ label: "审核信息", value: "check" },
{ label: "生产经营", value: "manage" },
{ label: "结算报销", value: "account" },
],
},
],
},
{
to: [],
top: 0,
left: 0,
status: "loading",
isSource: false,
isTarget: false,
config: [
{
label: "名称",
name: "label",
type: "text",
value: "完结节点2",
require: true,
},
{
label: "描述",
name: "description",
type: "textarea",
value: "",
require: false,
},
{
label: "归属",
name: "affiliation",
type: "select",
value: "check",
require: true,
options: [
{ label: "审核信息", value: "check" },
{ label: "生产经营", value: "manage" },
{ label: "结算报销", value: "account" },
],
},
],
},
],
},
]);
//渲染节点信息(默认是后台传过来的)
let info = ref([]);
//新增一个节点
const addNode = (newInfo) => {
newInfo.id = uuidv4();
newInfo = Object.assign(newInfo, globalConfig);
info.value.push(newInfo);
console.log(newInfo, "???新增节点的信息");
// makeFun(newInfo)
nextTick(() => {
renderFlag.value = "new";
makeFun(newInfo);
});
};
//新增一条连线
const addLine = () => {
info.value[3].to = ["div6"];
renderNode();
};
const mouseDownFun = (event) => {
//具体位置鼠标信息
let mousedownPositionInfo = { x: event.clientX, y: event.clientY };
//被拖拽节点初始的位置信息
let moveBoxBeforePosition = {
x: event.target.getBoundingClientRect().x,
y: event.target.getBoundingClientRect().y,
};
nodePositionDiff = {
leftDiff: mousedownPositionInfo.x - moveBoxBeforePosition.x,
topDiff: mousedownPositionInfo.y - moveBoxBeforePosition.y,
};
};
//开始拖动
const moveStart = (el) => {
console.log(el, "开始拖动");
};
//停止拖动
const moveEnd = (el) => {
refreshPlumbPostionInfo();
let dragNodeInfo = JSON.parse(el.item.attributes.divOption.nodeValue);
judgePosition(
dragNodeInfo,
plumbBoxPositionInfo,
el.originalEvent.x,
el.originalEvent.y
);
};
//判断拖动区域
const judgePosition = (dragNodeInfo, plumbBoxPositionInfo, x, y) => {
//拖拽至画布外部
if (
x - nodePositionDiff.leftDiff < plumbBoxPositionInfo.left ||
x + 180 - nodePositionDiff.leftDiff > plumbBoxPositionInfo.right ||
y - nodePositionDiff.topDiff < plumbBoxPositionInfo.top ||
y + 40 - nodePositionDiff.topDiff > plumbBoxPositionInfo.bottom
) {
ElMessage({
message: "节点不能拖拽至画布之外",
type: "error",
});
} else {
dragNodeInfo.left =
x - plumbBoxPositionInfo.left - nodePositionDiff.leftDiff;
dragNodeInfo.top = y - plumbBoxPositionInfo.top - nodePositionDiff.topDiff;
addNode(dragNodeInfo);
}
};
//刷新画布区域信息
const refreshPlumbPostionInfo = () => {
plumbBox = document.querySelector(".plumbBox");
let positionInfo = plumbBox.getBoundingClientRect();
plumbBoxPositionInfo = positionInfo;
};
//渲染节点
const renderNode = (flag) => {
//合并节点信息和配置
info.value.map((item) => (item = Object.assign(item, globalConfig)));
//这里需要等所依赖的DOM节点全部渲染完毕,才能进行图形渲染
nextTick(() => {
if (flag === "new") {
renderFlag.value = "once";
}
plumbInit.deleteEveryConnection();
plumbInit.deleteEveryEndpoint();
refreshPlumbPostionInfo();
//渲染画布中的信息节点
let renderList = [];
// if(info.value.length<1){return}
info.value.forEach((item) => {
if (item.to.length > 0) {
item.to.forEach((v) => {
renderList.push({
source: item.id,
target: v,
anchor: item.anchor,
connector: item.connector,
endpoint: item.endpoint,
overlays: item.overlays,
paintStyle: item.paintStyle,
hoverPaintStyle: item.hoverPaintStyle,
endpointStyle: item.endpointStyle,
});
});
}
});
plumbList.value = renderList;
//渲染函数
plumbInit.ready(() => {
renderList.forEach((item) => {
// plumbInit.connect(item,jsPlumbConnectOptions);
plumbInit.connect(item);
});
info.value.forEach((item) => {
makeFun(item);
plumbInit.draggable(item.id, {
containment: "parent",
stop: function (el) {
item.left = el.pos[0];
item.top = el.pos[1];
},
});
});
});
});
};
//设置节点可连接属性
const makeFun = (item) => {
plumbInit.setSourceEnabled(item.id, item.isSource);
plumbInit.setTargetEnabled(item.id, item.isTarget);
plumbInit.setDraggable(item.id, true);
plumbInit.makeSource(item.id, {
filter: ".plumbNode",
filterExclude: false,
allowLoopback: true,
maxConnections: -1,
Container: "plumbBox",
anchor: item.anchor,
connector: item.connector,
endpoint: item.endpoint,
overlays: item.overlays,
paintStyle: item.paintStyle,
hoverPaintStyle: item.hoverPaintStyle,
endpointStyle: item.endpointStyle,
});
plumbInit.makeTarget(item.id, {
filter: ".plumbNode",
filterExclude: false,
allowLoopback: true,
maxConnections: 1,
Container: "plumbBox",
anchor: item.anchor,
connector: item.connector,
endpoint: item.endpoint,
overlays: item.overlays,
paintStyle: item.paintStyle,
hoverPaintStyle: item.hoverPaintStyle,
endpointStyle: item.endpointStyle,
});
plumbInit.draggable(item.id, {
containment: "parent",
stop: function (el) {
item.left = el.pos[0];
item.top = el.pos[1];
},
});
};
// 给元素设置渲染样式
const getStyle = function (item) {
return {
position: "absolute",
left: item.left + "px",
top: item.top + "px",
// color:item.color,
// border:'1px solid #',
width: "180px",
height: "36px",
lineHeight: "36px",
textAlign: "center",
borderLeft: "5px solid blue",
borderRadius: "4%",
boxShadow: "#eee 3px 3px 3px 3px",
cursor: "pointer",
};
};
//初始化jsplumb实例
let plumbInit = jsPlumb.getInstance();
//
plumbInit.bind("click", (conn, originalEvent) => {
console.log(conn, "点击连线");
let lineInfo = {};
console.log(info.value, "整体信息");
let sourceInfo = info.value.find((v) => v.id === conn.sourceId);
let targetInfo = info.value.find((v) => v.id === conn.targetId);
lineInfo = {
sourceInfo,
targetInfo,
};
rightForm.value.getLineInfo(lineInfo);
// console.log("点击了", coon, originalEvent);
// plumbInit.deleteConnection(conn);
});
//连线触发事件
plumbInit.bind("connection", (event) => {
// console.log(event, "新的连线事件触发");
// forceUpdate();
let sourceNode = info.value.find((item) => item.id === event.sourceId);
console.log(sourceNode.to, event.targetId, "???");
if (sourceNode.to.findIndex((v) => v === event.targetId) === -1) {
sourceNode.to.push(event.targetId);
}
plumbInit.repaint();
nextTick(() => {
renderFlag.value = "new";
});
if (renderFlag.value === "new") {
console.log("新的页面刷新");
renderFlag.value = "once";
renderNode("new");
}
// console.log(info.value,'所有节点')
// renderNode()
});
//切换动态节点
function sendActive(node) {
activeNode.value = node;
console.log(activeNode.value, "动态节点");
rightForm.value.changeFormData(activeNode.value.config);
}
onMounted(() => {
setTimeout(() => {
// info.value = [
// {
// name: "div1",
// to: ["div2", "div3"],
// top: 300,
// left: 100,
// color: "red",
// context: "开始运行",
// status: "success",
// isSource: true,
// isTarget: false,
// },
// {
// name: "div2",
// to: ["div4"],
// top: 200,
// left: 500,
// color: "green",
// context: "构建任务1",
// status: "success",
// isSource: true,
// isTarget: true,
// },
// {
// name: "div3",
// to: ["div5"],
// top: 400,
// left: 500,
// color: "green",
// context: "构建任务2",
// status: "error",
// isSource: true,
// isTarget: true,
// },
// {
// name: "div4",
// to: [],
// top: 200,
// left: 900,
// color: "blue",
// context: "完成部署1",
// status: "success",
// isSource: false,
// isTarget: true,
// },
// {
// name: "div5",
// to: [],
// top: 400,
// left: 900,
// color: "blue",
// context: "完成部署2",
// status: "loading",
// isSource: false,
// isTarget: true,
// },
// ]
renderNode();
nextTick(() => {
console.log("页面初次渲染完毕");
renderFlag.value = "render";
});
}, 2000);
});
//右侧保存值
const changeActiveNodeInfo = (info) => {
console.log(info, "保存后的新值");
activeNode.value.config = info;
nextTick(() => {
renderFlag.value = "new";
makeFun(activeNode.value);
});
};
//删除线
const deleteLine = (deleteLineInfo) => {
console.log(deleteLineInfo, "要删除的连线信息");
console.log(info.value, "全量信息");
let sourceIndex = info.value.findIndex(
(item) => item.id === deleteLineInfo.sourceInfo.id
);
let deleteTargetId = deleteLineInfo.targetInfo.id;
let deleteTargetIndex = info.value[sourceIndex].to.findIndex(
(v) => v === deleteTargetId
);
info.value[sourceIndex].to.splice(deleteTargetIndex, 1);
renderNode();
};
//删除节点
const deleteNode = (nodeInfo) => {
console.log(activeNode.value);
let nodeIndex = info.value.findIndex(
(item) => item.id === activeNode.value.id
);
info.value.splice(nodeIndex, 1);
info.value.forEach((item) => {
let flagIndex = item.to.findIndex((v) => v === activeNode.value.id);
if (flagIndex !== -1) {
item.to.splice(flagIndex, 1);
}
});
console.log(info.value, "节点列表");
renderNode();
activeNode.value = {};
rightForm.value.changeFormData([]);
};
//暴露给父组件的值,需要父组件发送请求
defineExpose({
plumbList,
info,
});
</script>
<style lang="less" scoped>
.box {
width: 100%;
height: 100%;
display: flex;
}
.leftMenu {
width: 240px;
border-right: 1px solid #d3d3d3;
border-bottom: 1px solid #d3d3d3;
h3 {
width: 100%;
height: 30px;
line-height: 30px;
background: #eee;
text-align: center;
}
.content {
width: 180px;
height: 40px;
border: dashed 1px #030303;
text-align: center;
line-height: 40px;
margin-bottom: 10px;
margin-right: 10px;
cursor: pointer;
}
}
.plumbBox {
overflow: scroll;
position: relative;
// margin: 0 20px;
width: 100%;
border-right: 1px solid #d3d3d3;
}
.rightContent {
width: 240px;
border-bottom: 1px solid #d3d3d3;
h3 {
width: 200px;
height: 30px;
line-height: 30px;
text-align: center;
}
}
.plumbNode {
float: left;
line-height: 45px;
}
.activePlumbNode {
float: left;
line-height: 45px;
background: #0bcfe9;
}
.normalNode {
background-color: #fff;
}
.activeNode {
background-color: #80eaf8;
}
</style>
子组件(右侧信息栏)
<template>
<el-form
v-if="infoType === 'node'"
ref="ruleFormRef"
:model="ruleForm"
:rules="rules"
label-width="120px"
class="demo-ruleForm"
:size="formSize"
status-icon
label-position="left"
>
<el-form-item
:label="item.label"
:prop="item.name"
v-for="(item, index) in formData"
:key="index"
>
<el-input v-model="ruleForm[item.name]" v-if="item.type === 'text'" />
<el-input
v-model="ruleForm[item.name]"
type="textarea"
v-else-if="item.type === 'textarea'"
/>
<el-select
v-model="ruleForm[item.name]"
v-else-if="item.type === 'select'"
>
<el-option
:label="m.label"
:value="m.value"
v-for="(m, n) in item.options"
:key="n"
/>
</el-select>
</el-form-item>
</el-form>
<div v-if="infoType === 'line'">
<h6>起始节点:</h6>
<p>{{ lineData.sourceInfo.config[0].value }}</p>
<h6>目标节点:</h6>
<p>{{ lineData.targetInfo.config[0].value }}</p>
</div>
<div class="btnBox" v-show="listShow">
<el-button
type="primary"
@click="submitForm(ruleFormRef)"
v-show="infoType === 'node' && formData.length > 0"
>
保存节点
</el-button>
<el-button
type="primary"
v-show="infoType === 'node' && formData.length > 0"
@click="delteNodeFun"
>
删除节点
</el-button>
<el-button
type="primary"
v-show="infoType === 'line'"
@click="deleteLineFun"
>
删除连线
</el-button>
</div>
</template>
<script setup>
let formData = ref([]);
let lineData = ref({});
const formSize = ref("default");
const ruleFormRef = ref();
const listShow = ref(false);
let ruleForm = reactive({});
const infoType = ref("node");
const emit = defineEmits(["changeActiveNodeInfo", "deleteLine", "deleteNode"]);
const rules = reactive({
label: [
{ required: true, message: "节点名称为必输项", trigger: "blur" },
{ min: 4, max: 8, message: "请输入4至8之间的字符", trigger: "blur" },
],
affiliation: [
{
required: true,
message: "请选择归属项",
trigger: "change",
},
],
});
const submitForm = async (formEl) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
console.log(valid, ruleForm);
formData.value.forEach((item) => {
item.value = ruleForm[item.name];
});
emit("changeActiveNodeInfo", formData.value);
} else {
console.log("error submit!", fields);
}
});
};
const resetForm = (formEl) => {
if (!formEl) return;
formEl.resetFields();
};
const options = Array.from({ length: 10000 }).map((_, idx) => ({
value: `${idx + 1}`,
label: `${idx + 1}`,
}));
const getFormData = () => {
console.log(formData, "信息");
};
//父节点传递信息函数
const changeFormData = (val) => {
infoType.value = "node";
listShow.value = true;
console.log(val);
formData.value = val;
formData.value.forEach((item) => {
console.log(item, "遍历");
ruleForm[item.name] = item.value;
});
console.log(ruleForm, "???");
};
//父节点传递连线信息
const getLineInfo = (lineInfo) => {
listShow.value = true;
console.log(lineInfo, "连线信息");
infoType.value = "line";
lineData.value = lineInfo;
};
//删除连线信息
const deleteLineFun = () => {
emit("deleteLine", lineData.value);
infoType.value = null;
};
//删除节点
const delteNodeFun = () => {
emit("deleteNode");
};
onMounted(() => {});
defineExpose({
changeFormData,
getLineInfo,
formData,
});
</script>
<style lang="less" scoped>
/deep/ .el-form-item {
display: flex;
flex-direction: column !important;
}
h6 {
font-size: 14px;
font-weight: 500;
margin: 5px;
overflow: hidden;
}
p {
margin: 20px;
border-bottom: 1px solid #eee;
overflow: hidden;
}
</style>
vue3的项目可以直接引用如上的代码,下载相关依赖就可以运行了.文章来源地址https://www.toymoban.com/news/detail-759913.html
到了这里,关于jsPlumb的学习使用(三):常规流程图完成的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!