我们可以基于 Vue 组件自定义边,可以在边上添加任何想要的 Vue 组件,甚至将原有的边通过样式隐藏,重新绘制。
如 Example3 中所示:
锚点
默认情况下,LogicFlow 只记录节点与节点的信息。但是在一些业务场景下,需要关注到锚点,比如在 UML 类图中的关联关系;或者锚点表示节点的入口和出口之类。这个时候需要重写连线的保存方法,将锚点信息也一起保存。
class CustomEdgeModel2 extends LineEdgeModel {
// 重写此方法,使保存数据是能带上锚点数据。
getData() {
const data = super.getData();
data.sourceAnchorId = this.sourceAnchorId;
data.targetAnchorId = this.targetAnchorId;
return data;
}
}
动画
由于 LogicFlow 是基于 svg 的流程图编辑框架,所以我们可以给 svg 添加动画的方式来给流程图添加动画效果。为了方便使用,我们也内置了基础的动画效果。在定义边的时候,可以将属性isAnimation
设置为 true 就可以让边动起来,也可以使用lf.openEdgeAnimation(edgeId)
来开启边的默认动画。
class CustomEdgeModel extends PolylineEdgeModel {
setAttributes() {
this.isAnimation = true;
}
getEdgeAnimationStyle() {
const style = super.getEdgeAnimationStyle();
style.strokeDasharray = "5 5";
style.animationDuration = "10s";
return style;
}
}
下面我们对上面的内容写一个简单的样例:
样例中使用了 JSX 所以需要进行配置,在项目中,运行pnpm install @vitejs/plugin-vue-jsx
并在vite.config.js
增加如下配置:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig({
plugins: [vue(), vueJsx()]
});
新建src/views/Example/LogicFlowAdvance/Edge/Example01/CustomCard.vue
代码如下:
<script setup lang="tsx">
import { ref } from 'vue'
const props = defineProps({
properties: {
type: Object,
required: true
}
})
type Answer = {
text: string
id: string
}
type Properties = {
title: string
content: string
answers: Answer[]
}
// Example props passed to the component
const properties = ref(props.properties as Properties)
</script>
<template>
<div class="html-card">
<!-- <ElButton οnclick="alert(123)" type="primary" style="margin-left: 15px">Title</ElButton> -->
<div class="html-card-header">{{ properties.title }}</div>
<div class="html-card-body">{{ properties.content }}</div>
<div class="html-card-footer">
<div v-for="answer in properties.answers" :key="answer.id" class="html-card-label">
{{ answer.text }}
</div>
</div>
</div>
</template>
<style scoped>
.html-card {
width: 240px;
height: 100%;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
box-sizing: border-box;
padding: 5px;
}
/* 定义节点不被允许连接的时候,节点样式 */
.lf-node-not-allow .html-card {
border-color: #f56c6c;
}
.lf-node-allow .html-card {
border-color: #67c23a;
}
.html-card-header {
font-size: 12px;
line-height: 24px;
margin-left: 14px;
}
.html-card-header:before {
content: '';
position: absolute;
left: 5px;
top: 13px;
display: block;
width: 7px;
height: 7px;
border: 1px solid #cbcef5;
border-radius: 6px;
}
.html-card-body {
font-size: 12px;
color: #6f6a6f;
margin-top: 5px;
}
.html-card-footer {
display: flex;
position: absolute;
bottom: 5px;
}
.html-card-label {
font-size: 12px;
line-height: 16px;
padding: 2px;
background: #ebeef5;
margin-right: 10px;
}
</style>
新建src/views/Example/LogicFlowAdvance/Edge/Example01/CustomCard.tsx
代码如下:
import { HtmlNode, HtmlNodeModel } from '@logicflow/core'
import { createApp, h, App, VNode, render } from 'vue'
import CustomCard from './CustomCard.vue'
class HtmlCard extends HtmlNode {
isMounted: boolean
app: App<Element>
r: VNode
constructor(props: any) {
super(props)
this.isMounted = false
this.r = h(CustomCard, {
properties: props.model.getProperties(),
text: props.model.inputData
})
this.app = createApp({
render: () => this.r
})
}
// 重写HtmlNode的setHtml,来控制html节点内容。
setHtml(rootEl: HTMLElement) {
if(!this.isMounted) {
this.isMounted = true
const node = this.getCardEl()
render(node, rootEl)
} else {
if (this.r.component) {
this.r.component.props.properties = this.props.model.getProperties();
}
}
}
getCardEl() {
const { properties } = this.props.model
return <><CustomCard properties={properties} /></>
}
}
class HtmlCardModel extends HtmlNodeModel {
initNodeData(data: any) {
super.initNodeData(data)
// 禁止节点文本可以编辑
this.text.editable = false
this.width = 240
// 定义连接规则,只允许出口节点连接入口节点
const rule = {
message: '只允许出口节点连接入口节点',
validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => {
console.log(sourceNode, targetNode)
console.log(sourceAnchor, targetAnchor)
return sourceAnchor.type === 'sourceAnchor' && targetAnchor.type === 'targetAnchor'
}
}
this.sourceRules.push(rule)
}
setAttributes() {
const {
properties: { content }
} = this
// 动态计算节点的高度
const rowSize = Math.ceil(content.length / 20)
this.height = 60 + rowSize * 18
}
/**
* 计算每个锚点的位置
*/
getDefaultAnchor() {
const { height, x, y, id, properties } = this
const anchorPositon = []
anchorPositon.push({
x,
y: y - height / 2,
type: 'targetAnchor',
id: `${id}_targetAnchor`
})
if (properties.answers) {
let preOffset = 5
properties.answers.forEach((answer: any) => {
const text = answer.text
// 计算每个锚点的位置,锚点的位置一般相对节点中心点进行偏移
const offsetX = preOffset + (this.getBytesLength(text) * 6 + 4) / 2 - this.width / 2
preOffset += this.getBytesLength(text) * 6 + 4 + 10
const offsetY = height / 2
anchorPositon.push({
x: x + offsetX,
y: y + offsetY,
type: 'sourceAnchor',
id: answer.id
})
})
}
return anchorPositon
}
getBytesLength(word: any) {
if (!word) {
return 0
}
let totalLength = 0
for (let i = 0; i < word.length; i++) {
const c = word.charCodeAt(i)
if (word.match(/[A-Z]/)) {
totalLength += 1.5
} else if ((c >= 0x0001 && c <= 0x007e) || (c >= 0xff60 && c <= 0xff9f)) {
totalLength += 1.2
} else {
totalLength += 2
}
}
return totalLength
}
}
export default {
type: 'html-card',
view: HtmlCard,
model: HtmlCardModel
}
新建src/views/Example/LogicFlowAdvance/Edge/Example01/CustomEdge.tsx
代码如下:
import { BezierEdge, BezierEdgeModel } from '@logicflow/core'
class CustomEdge extends BezierEdge {}
class CustomEdgeModel extends BezierEdgeModel {
getEdgeStyle() {
const style = super.getEdgeStyle()
// svg属性
style.strokeWidth = 1
style.stroke = '#ababac'
return style
}
/**
* 重写此方法,使保存数据是能带上锚点数据。
*/
getData() {
const data: any = super.getData()
data.sourceAnchorId = this.sourceAnchorId
data.targetAnchorId = this.targetAnchorId
return data
}
setAttributes() {
this.isAnimation = true;
}
}
export default {
type: 'custom-edge',
view: CustomEdge,
model: CustomEdgeModel
}
新建src/views/Example/LogicFlowAdvance/Edge/Example01/data.ts
,内容如下:
const data = {
nodes: [
{
id: 'node_id_1',
type: 'html-card',
x: 340,
y: 100,
properties: {
title: '普通话术',
content: '喂,您好,这里是XX装饰,专业的装修品牌。请问您最近有装修吗?',
answers: [
{ id: '1', text: '装好了' },
{ id: '2', text: '肯定' },
{ id: '3', text: '拒绝' },
{ id: '4', text: '否定' },
{ id: '5', text: '默认' }
]
}
},
{
id: 'node_id_2',
type: 'html-card',
x: 160,
y: 300,
properties: {
title: '推荐话术',
content:
'先生\\女士,您好!几年来,我们通过对各种性质的建筑空间进行设计和施工,使我们积累了丰富的管理、设计和施工经验,公司本着以绿色环保为主题,对家居住宅、办公、商铺等不同特点的室内装饰产品形成了独特的装饰理念。',
answers: [
{ id: '1', text: '感兴趣' },
{ id: '2', text: '不感兴趣' },
{ id: '3', text: '拒绝' }
]
}
},
{
id: 'node_id_3',
type: 'html-card',
x: 480,
y: 260,
properties: { title: '结束话术', content: '抱歉!打扰您了!', answers: [] }
},
{
id: 'node_id_4',
type: 'html-card',
x: 180,
y: 500,
properties: {
title: '结束话术',
content: '好的,我们将安排师傅与您联系!',
answers: []
}
}
],
edges: [
{
id: 'e54d545f-3381-4769-90ef-0ee469c43e9c',
type: 'custom-edge',
sourceNodeId: 'node_id_1',
targetNodeId: 'node_id_2',
startPoint: { x: 289, y: 148 },
endPoint: { x: 160, y: 216 },
properties: {},
pointsList: [
{ x: 289, y: 148 },
{ x: 289, y: 248 },
{ x: 160, y: 116 },
{ x: 160, y: 216 }
],
sourceAnchorId: '2',
targetAnchorId: 'node_id_2_targetAnchor'
},
{
id: 'ea4eb652-d5de-4a85-aae5-c38ecc013fe6',
type: 'custom-edge',
sourceNodeId: 'node_id_2',
targetNodeId: 'node_id_4',
startPoint: { x: 65, y: 384 },
endPoint: { x: 180, y: 461 },
properties: {},
pointsList: [
{ x: 65, y: 384 },
{ x: 65, y: 484 },
{ x: 180, y: 361 },
{ x: 180, y: 461 }
],
sourceAnchorId: '1',
targetAnchorId: 'node_id_4_targetAnchor'
},
{
id: 'da216c9e-6afe-4472-baca-67d98abb1d31',
type: 'custom-edge',
sourceNodeId: 'node_id_1',
targetNodeId: 'node_id_3',
startPoint: { x: 365, y: 148 },
endPoint: { x: 480, y: 221 },
properties: {},
pointsList: [
{ x: 365, y: 148 },
{ x: 365, y: 248 },
{ x: 480, y: 121 },
{ x: 480, y: 221 }
],
sourceAnchorId: '4',
targetAnchorId: 'node_id_3_targetAnchor'
},
{
id: '47e8aff3-1124-403b-8c64-78d94ec03298',
type: 'custom-edge',
sourceNodeId: 'node_id_1',
targetNodeId: 'node_id_3',
startPoint: { x: 327, y: 148 },
endPoint: { x: 480, y: 221 },
properties: {},
pointsList: [
{ x: 327, y: 148 },
{ x: 327, y: 248 },
{ x: 476, y: 161 },
{ x: 480, y: 221 }
],
sourceAnchorId: '3',
targetAnchorId: 'node_id_3_targetAnchor'
}
]
}
export default data
最后新建src/views/Example/LogicFlowAdvance/Edge/Example01/Example01.vue
内容如下:
<script setup lang="ts">
import LogicFlow from '@logicflow/core'
import '@logicflow/core/dist/style/index.css'
import { onMounted } from 'vue'
import data from './data'
import CustomCard from './CustomCard'
import CustomEdge from './CustomEdge'
import CustomEdge2 from './CustomEdge2'
// 在组件挂载时执行
onMounted(() => {
// 创建 LogicFlow 实例
const lf = new LogicFlow({
container: document.getElementById('container')!, // 指定容器元素
grid: true // 启用网格
})
lf.register(CustomCard)
lf.register(CustomEdge)
lf.register(CustomEdge2)
lf.setDefaultEdgeType('custom-edge')
lf.render(data)
})
</script>
<template>
<h3>Example01</h3>
<div id="container"></div>
<!-- 用于显示 LogicFlow 图表的容器 -->
</template>
<style>
#container {
/* 容器宽度 */
width: 100%;
/* 容器高度 */
height: 600px;
}
</style>
样例运行如下: