使用流程图插件 vue-super-flow 制作文字拖拽填充效果
vue项目在 html 中加载流程图,使用 vue-super-flow 工具,实现拖拽替换功能
·
背景
最近做了一个在线考试系统,其中有个题型是流程图类型的,考试端大概效果就是把答案选项拖拽到流程图框中,要求能拖动左侧选项,但是不能编辑右侧流程框和连接线。
需要满足简单流程图编辑,最主要是要满足在后台中流程图题型动态配置性,在考试端流程图的展示和答案选项的拖拽。
最开始的想法是把流程图当作背景,然后通过 CSS 相对定位一个个调整元素位置,使用拖拽工具给元素绑定拖拽事件,效果也还能凑合,但是如果流程图类型太多,难以维护,并且不支持后台动态配置。
如果是流程图的拖拽、编辑,基本大多数流程图工具都支持,但是这个拖拽选项到流程图框上填充内容的小小需求没有找到解决方案,那就只能自己解决了,选择的是vue-super-flow
这个工具,因为只需要简单的流程图编辑能力,并且这个工具可以在引用时灵活可配置。
觉得挺有意思,记录一下考试端实现过程
参考
vue-super-flow
vue-super-flow的使用
实现过程
主要讲一下这个 flowChart 组件,上代码
// flowChart.vue
<template>
<div class="f-super-flow">
<div class="f-node-container">
<div
class="f-node-item"
:class="!item.label ? 'f-node-item-empty' : ''"
v-for="(item, index) in nodeItemList"
:key="index"
@mousedown="evt => nodeItemMouseDown(evt, item.value, index)"
>
{{ item.label }}
</div>
</div>
<div class="f-flow-container" ref="flowContainer">
<super-flow
ref="superFlow"
:draggable="false"
:linkAddable="false"
:linkEditable="false"
:hasMarkLine="false"
:link-desc="linkDesc"
:node-list="nodeList"
:link-list="linkList"
>
<template v-slot:node="{ meta }">
<div
v-if="meta.name"
class="f-node-del"
:style="meta.type == 'judge' ? 'top: 10%;' : ''"
@mouseup="evt => nodeMouseUp(evt, meta)"
>x</div>
<div
class="f-flow-node"
:class="meta.type? `f-flow-node-${meta.type}`: ''"
>
<div class="f-node-content" :title="meta.name">{{ meta.name }}</div>
</div>
</template>
</super-flow>
</div>
</div>
</template>
<script>
/**
* 流程图题
*/
import SuperFlow from "vue-super-flow";
import "vue-super-flow/lib/index.css";
export default {
name: "flow-chart",
components: {
SuperFlow,
},
props: {
optionList: {
type: Array,
default: () => [],
},
flowChartNode: {
type: Array,
default: () => [],
},
flowChartLink: {
type: Array,
default: () => [],
}
},
data() {
return {
// flowChart 节点
nodeList: [],
// 线条
linkList: [],
// 左侧列表
nodeItemList: [],
dragConf: {
isDown: false,
isMove: false,
offsetTop: 0,
offsetLeft: 0,
clientX: 0,
clientY: 0,
ele: null,
info: null,
},
resetNodeItem: {
label: '',
value: {
meta: {
label: '',
name: '',
type: ''
}
}
}
}
},
computed: {
},
mounted() {
// 测试数据
this.nodeItemList = [
{
label: 1,
value: {
meta: {
label: 1,
name: 1
}
}
},
{
label: 3,
value: {
meta: {
label: 3,
name: 3
}
}
},
{
label: 2,
value: {
meta: {
label: 2,
name: 2
}
}
},
{
label: 5,
value: {
meta: {
label: 5,
name: 5
}
}
},
{
label: 4,
value: {
meta: {
label: 4,
name: 4
}
}
}
]
this.nodeList = [
{
id: "N1",
width: 180,
height: 50,
coordinate: [100, 32],
meta: {
label: "",
name: "",
type: "startAndEnd",
},
},
{
id: "N2",
width: 180,
height: 50,
coordinate: [100, 139],
meta: {
label: "",
name: "",
type: "process",
},
},
{
id: "N3",
width: 180,
height: 180,
coordinate: [100, 236],
meta: {
label: "",
name: "",
type: "judge",
},
},
{
id: "N4",
width: 180,
height: 50,
coordinate: [100, 500],
meta: {
label: "",
name: "",
type: "process",
},
},
{
id: "N5",
width: 180,
height: 50,
coordinate: [360, 300],
meta: {
label: "",
name: "",
type: "process",
},
},
{
id: "N6",
width: 180,
height: 50,
coordinate: [100, 596],
meta: {
label: "",
name: "",
type: "startAndEnd",
},
},
]
this.linkList = [
{
id: "linkUkwVhocoTp08AbLQ",
startId: "N1",
endId: "N2",
startAt: [60, 40],
endAt: [100, 0],
meta: null,
},
{
id: "linkS27wPzJ1Z7plttsR",
startId: "N3",
endId: "N5",
startAt: [168, 84],
endAt: [0, 20],
meta: { desc: "NO" },
},
{
id: "linka8cGGQAPQTtXuYID",
startId: "N5",
endId: "N2",
startAt: [100, 0],
endAt: [200, 20],
meta: null,
},
{
id: "linkdNLEL6EcIVijSQx4",
startId: "N2",
endId: "N3",
startAt: [100, 40],
endAt: [84, 0],
meta: null,
},
{
id: "link3heD5DMOJbmxcLHu",
startId: "N3",
endId: "N4",
startAt: [84, 168],
endAt: [100, 0],
meta: { desc: "YES" },
},
{
id: "linkVpTa6NUNNcG2NKeY",
startId: "N4",
endId: "N6",
startAt: [100, 40],
endAt: [60, 0],
meta: null,
},
]
document.addEventListener("mousemove", this.docMousemove);
document.addEventListener("mouseup", this.docMouseup);
this.$once("hook:beforeDestroy", () => {
document.removeEventListener("mousemove", this.docMousemove);
document.removeEventListener("mouseup", this.docMouseup);
});
},
methods: {
linkDesc(link) {
return link.meta ? link.meta.desc : "";
},
docMousemove({ clientX, clientY }) {
const conf = this.dragConf;
if (conf.isMove) {
conf.ele.style.top = clientY - conf.offsetTop + "px";
conf.ele.style.left = clientX - conf.offsetLeft + "px";
} else if (conf.isDown) {
// 鼠标移动量大于 5 时 移动状态生效
conf.isMove =
Math.abs(clientX - conf.clientX) > 5 ||
Math.abs(clientY - conf.clientY) > 5;
}
},
docMouseup({ clientX, clientY }) {
const conf = this.dragConf;
conf.isDown = false;
if (conf.isMove) {
const { top, right, bottom, left } = this.$refs.flowContainer.getBoundingClientRect();
// 判断鼠标是否进入 flow container
if (
clientX > left &&
clientX < right &&
clientY > top &&
clientY < bottom
) {
// 获取拖动元素左上角相对 super flow 区域原点坐标
const coordinate = this.$refs.superFlow.getMouseCoordinate( clientX, clientY );
for(let i = 0;i<this.nodeList.length;i++) {
let nodeData = this.nodeList[i]
let xStartPostion = nodeData.coordinate[0]
let yStartPostion = nodeData.coordinate[1] * 0.5
let xEndPostion = +nodeData.width + nodeData.coordinate[0]
let yEndPostion = +nodeData.height + nodeData.coordinate[1]
if((coordinate[0] >= xStartPostion && coordinate[0] <= xEndPostion) && (coordinate[1] >= yStartPostion && coordinate[1] <= yEndPostion)) {
nodeData.meta.label = conf.info.meta.label
nodeData.meta.name = conf.info.meta.name
nodeData.meta.index = conf.info.meta.index
this.$set(this.nodeList,i,nodeData)
this.nodeItemList[conf.info.meta.index] = this.resetNodeItem
break;
}
}
}
conf.isMove = false;
}
if (conf.ele) {
conf.ele.remove();
conf.ele = null;
}
},
nodeItemMouseDown(evt, infoFun, index) {
const { clientX, clientY, currentTarget } = evt;
const { top, left } = evt.currentTarget.getBoundingClientRect();
const conf = this.dragConf;
const ele = currentTarget.cloneNode(true);
infoFun.meta.index = index
Object.assign(this.dragConf, {
offsetLeft: clientX - left,
offsetTop: clientY - top,
clientX: clientX,
clientY: clientY,
info: infoFun,
ele,
isDown: true,
});
ele.style.position = "fixed";
ele.style.margin = "0";
ele.style.top = clientY - conf.offsetTop + "px";
ele.style.left = clientX - conf.offsetLeft + "px";
this.$el.appendChild(this.dragConf.ele);
},
nodeMouseUp(evt, meta) {
evt.preventDefault()
const metaData = { ...meta }
let selectIndex = this.nodeList.findIndex(item => item.meta.name === meta.name)
let nodeItem = this.nodeList[selectIndex]
nodeItem.meta.label = ""
nodeItem.meta.name = ""
this.$set(this.nodeList, selectIndex, nodeItem)
this.$set(this.nodeItemList, metaData.index, {
label: metaData.label,
value: {
meta: {
label: metaData.label,
name: metaData.name,
}
}
})
},
getNodeList() {
let list = this.$refs.superFlow.toJSON().nodeList
let arr = []
list.map(item => {
arr.push({
id: item.id,
name: item.meta.name
})
})
// console.log(arr);
return arr
},
},
};
</script>
<style lang="scss" scoped>
.f-super-flow {
position: relative;
display: flex;
background: #f5f5f5;
height: 100%;
.f-node-container {
width: 20%;
text-align: center;
background-color: #ffffff;
overflow: auto;
.f-node-item {
margin: 20px 20%;
padding: 6px 0;
border: 2px solid #333;
border-radius: 6px;
min-height: 40px;
font-size: 18px;
color: #333;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.3);
cursor: pointer;
user-select: none; // 防止鼠标左键拖动选中页面的文字
&:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.4);
}
&.f-node-item-empty {
border: 1px solid rgba(118, 118, 118, 0.3);
pointer-events: none;
background: rgba(239, 239, 239, 0.3);
cursor: no-drop;
}
}
}
.f-flow-container {
flex: 1;
// padding: 10px;
.super-flow {
overflow: auto;
}
}
/deep/.super-flow__node {
display: table;
border: none;
background: none;
box-shadow: none;
&:hover {
.f-node-del {
display: block;
}
}
.f-node-del {
display: none;
position: absolute;
right: -10px;
top: -10px;
width: 26px;
height: 26px;
line-height: 26px;
font-size: 20px;
text-align: center;
color: #666;
border-radius: 50%;
background: rgba(150,150,150,.5);
z-index: 1;
}
.f-flow-node {
position: relative;
display: table-cell;
width: 100%;
height: 100%;
vertical-align: middle;
font-size: 16px;
color: #333;
font-weight: bold;
box-sizing: border-box;
background: #fff;
.f-node-content {
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre-wrap;
}
}
.f-flow-node-startAndEnd {
border: 2px solid rgba(39, 107, 232, .6);
border-radius: 22px;
}
.f-flow-node-process {
border: 2px solid rgba(39, 107, 232, .6);
}
.f-flow-node-judge {
padding: 0 40px;
border: 2px solid rgba(39, 107, 232, .6);
transform: rotate(45deg) scale(.7);
.f-node-content {
transform: rotate(-45deg) scale(1.4);
}
}
.f-flow-node-quote {
padding: 0 40px;
border: 2px solid rgba(39, 107, 232, .6);
border-radius: 50%;
}
}
}
</style>
代码解析
因为不需要对流程图进行操作,先流程图的编辑功能相关配置设置为false
,vue-super-flow
Demo 里有绑定鼠标事件,不过不支持拖拽到流程图框上面替换,利用已有的鼠标事件做修改。
自定义流程图框型样式
<template v-slot:node="{ meta }">
<div
v-if="meta.name"
class="f-node-del"
:style="meta.type == 'judge' ? 'top: 10%;' : ''"
@mouseup="evt => nodeMouseUp(evt, meta)"
>x</div>
<div
class="f-flow-node"
:class="meta.type? `f-flow-node-${meta.type}`: ''"
>
<div class="f-node-content" :title="meta.name">{{ meta.name }}</div>
</div>
</template>
左侧选项拖拽到流程图上填充
鼠标是否进入 flow container 之后,循环当前节点列表,判断当前鼠标位置是否在流程图框的范围内,在范围内时设置nodeList
节点内容,并重置nodeItemList
选项禁止选中拖拽
// 获取当前鼠标在 graph 坐标系的坐标
const coordinate = this.$refs.superFlow.getMouseCoordinate( clientX, clientY );
for(let i = 0;i<this.nodeList.length;i++) {
let nodeData = this.nodeList[i]
let xStartPostion = nodeData.coordinate[0]
let yStartPostion = nodeData.coordinate[1] * 0.5
let xEndPostion = +nodeData.width + nodeData.coordinate[0]
let yEndPostion = +nodeData.height + nodeData.coordinate[1]
if((coordinate[0] >= xStartPostion && coordinate[0] <= xEndPostion) && (coordinate[1] >= yStartPostion && coordinate[1] <= yEndPostion)) {
nodeData.meta.label = conf.info.meta.label
nodeData.meta.name = conf.info.meta.name
nodeData.meta.index = conf.info.meta.index
this.$set(this.nodeList,i,nodeData)
this.nodeItemList[conf.info.meta.index] = this.resetNodeItem
break;
}
}
删除流程图框中内容
拖拽到流程图框中的内容支持删除,删除操作绑定mouseup
事件上,尝试过绑定click
事件无效。
@mouseup="evt => nodeMouseUp(evt, meta)"
// 删除事件
nodeMouseUp(evt, meta) {
evt.preventDefault()
const metaData = { ...meta }
let selectIndex = this.nodeList.findIndex(item => item.meta.name === meta.name)
let nodeItem = this.nodeList[selectIndex]
nodeItem.meta.label = ""
nodeItem.meta.name = ""
this.$set(this.nodeList, selectIndex, nodeItem)
this.$set(this.nodeItemList, metaData.index, {
label: metaData.label,
value: {
meta: {
label: metaData.label,
name: metaData.name,
}
}
})
},
记录
更多推荐
已为社区贡献3条内容
所有评论(0)