一、前言
使用vueflow封装了一个层级关系组件。
二、官网
三、安装
方式一:papackage.json添加依赖后直接npm install
- @vue-flow/background@^1.3.0
- 组件名称:背景栅格组件
- 功能:为vue flow提供背景支持,通常用于显示栅格线或背景图案,以帮助用户更好地对齐和布局流程图中的元素。
- 版本:^1.3.0 表示该组件的版本号至少为1.3.0,但会兼容该版本之后的任何更新(遵循语义化版本控制规则)。
- @vue-flow/controls@^1.1.2
- 组件名称:控件组件
- 功能:提供用于缩放、平移和旋转流程图的控制元素。这些控件允许用户以交互方式调整流程图的视角和布局。
- 版本:^1.1.2 表示该组件的版本号至少为1.1.2,同样遵循语义化版本控制规则。
- @vue-flow/core@^1.41.2
- 组件名称:核心组件
- 功能:vue flow的核心功能组件,提供了创建和管理流程图所需的基础设施。这包括节点、边(连接线)、事件处理、状态管理等核心功能。
- 版本:^1.41.2 表示该组件的版本号至少为1.41.2,并兼容后续更新。
- @vue-flow/minimap@^1.5.0
- 组件名称:缩略图组件
- 功能:提供一个缩略图视图,用于显示整个流程图的概览。用户可以通过缩略图快速定位到流程图中的特定区域。
- 版本:^1.5.0 表示该组件的版本号至少为1.5.0,遵循语义化版本控制规则。
- @vue-flow/node-resizer@^1.4.0
- 组件名称:节点调整大小组件
- 功能:允许用户通过拖动边缘来调整节点的大小。这增加了流程图创建的灵活性和用户友好性。
- 版本:^1.4.0 表示该组件的版本号至少为1.4.0,同样遵循语义化版本控制规则。
- @vue-flow/node-toolbar@^1.1.0
- 组件名称:节点工具栏组件
- 功能:为节点提供附加的工具栏,通常包含用于编辑、删除或配置节点选项的按钮。这增强了流程图编辑的交互性和便捷性。
- 版本:^1.1.0 表示该组件的版本号至少为1.1.0,遵循语义化版本控制规则。
方式二:npm install @vue-flow
四、引用
1.app.vue文件
<script setup>
import { ref, toref } from 'vue'
import { minimap } from '@vue-flow/minimap'
import { position, vueflow } from '@vue-flow/core'
import colorselectornode from './colorselectornode.vue'
import outputnode from './outputnode.vue'
import { presets } from './presets.js'
const nodes = ref([
{
id: '1',
type: 'color-selector',
data: { color: presets.ayame },
position: { x: 0, y: 50 },
},
{
id: '2',
type: 'output',
position: { x: 350, y: 114 },
targetposition: position.left,
},
])
const edges = ref([
{
id: 'e1a-2',
source: '1',
sourcehandle: 'a',
target: '2',
animated: true,
style: {
stroke: presets.ayame,
},
},
])
const colorselectordata = toref(() => nodes.value[0].data)
// minimap stroke color functions
function nodestroke(n) {
switch (n.type) {
case 'input':
return '#0041d0'
case 'color-selector':
return n.data.color
case 'output':
return '#ff0072'
default:
return '#eee'
}
}
function nodecolor(n) {
if (n.type === 'color-selector') {
return n.data.color
}
return '#fff'
}
</script>
<template>
<vueflow
v-model:nodes="nodes"
:edges="edges"
class="custom-node-flow"
:class="[colorselectordata?.isgradient ? 'animated-bg-gradient' : '']"
:style="{ backgroundcolor: colorselectordata?.color }"
fit-view-on-init
>
<template #node-color-selector="props">
<colorselectornode :id="props.id" :data="props.data" />
</template>
<template #node-output>
<outputnode />
</template>
<minimap :node-stroke-color="nodestroke" :node-color="nodecolor" />
</vueflow>
</template>2.colorselectornode.vue
<script setup>
import { handle, position, usevueflow } from '@vue-flow/core'
import { colors } from './presets.js'
const props = defineprops({
id: {
type: string,
required: true,
},
data: {
type: object,
required: true,
},
})
const { updatenodedata, getconnectededges } = usevueflow()
function onselect(color) {
updatenodedata(props.id, { color, isgradient: false })
const connectededges = getconnectededges(props.id)
for (const edge of connectededges) {
edge.style = {
stroke: color,
}
}
}
function ongradient() {
updatenodedata(props.id, { isgradient: true })
}
</script>
<template>
<div>select a color</div>
<div class="color-selector nodrag nopan">
<button
v-for="{ name: colorname, value: color } of colors"
:key="colorname"
:title="colorname"
:class="{ selected: color === data.color }"
:style="{ backgroundcolor: color }"
type="button"
@click="onselect(color)"
/>
<button class="animated-bg-gradient" title="gradient" type="button" @click="ongradient" />
</div>
<handle id="a" type="source" :position="position.right" />
</template>3.outputnode.vue
<script setup>
import { handle, position, usehandleconnections, usenodesdata } from '@vue-flow/core'
const connections = usehandleconnections({
type: 'target',
})
const nodesdata = usenodesdata(() => connections.value[0]?.source)
</script>
<template>
<handle
type="target"
:position="position.left"
:style="{ height: '16px', width: '6px', backgroundcolor: nodesdata.data?.color, filter: 'invert(100%)' }"
/>
{{ nodesdata.data?.isgradient ? 'gradient' : nodesdata.data?.color }}
</template>4.presets.js
export const presets = {
sumi: '#1c1c1c',
gofun: '#fffffb',
byakuroku: '#a8d8b9',
mizu: '#81c7d4',
asagi: '#33a6b8',
ukon: '#efbb24',
mushikuri: '#d9cd90',
hiwa: '#bec23f',
ichigo: '#b5495b',
kurenai: '#cb1b45',
syojyohi: '#e83015',
konjyo: '#113285',
fuji: '#8b81c3',
ayame: '#6f3381',
torinoko: '#dac9a6',
kurotsurubami: '#0b1013',
ohni: '#f05e1c',
kokikuchinashi: '#fb9966',
beniukon: '#e98b2a',
sakura: '#fedfe1',
toki: '#eea9a9',
}
export const colors = object.keys(presets).map((color) => {
return {
name: color,
value: presets[color],
}
})5.main.css
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/core@1.41.2/dist/style.css';
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/core@1.41.2/dist/theme-default.css';
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/controls@latest/dist/style.css';
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/minimap@latest/dist/style.css';
@import 'https://cdn.jsdelivr.net/npm/@vue-flow/node-resizer@latest/dist/style.css';
html,
body,
#app {
margin: 0;
height: 100%;
}
#app {
text-transform: uppercase;
font-family: 'jetbrains mono', monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
.vue-flow__minimap {
transform: scale(75%);
transform-origin: bottom right;
}
.vue-flow__edges {
filter:invert(100%)
}
.vue-flow__handle {
height:24px;
width:8px;
border-radius:4px
}
.vue-flow__node-color-selector {
border:1px solid #777;
padding:10px;
border-radius:10px;
background:#f5f5f5;
display:flex;
flex-direction:column;
justify-content:space-between;
align-items:center;
gap:10px;
max-width:250px
}
.vue-flow__node-color-selector .color-selector {
display:flex;
flex-direction:row;
flex-wrap:wrap;
justify-content:center;
max-width:90%;
margin:auto;
gap:4px
}
.vue-flow__node-color-selector .color-selector button {
border:none;
cursor:pointer;
padding:5px;
width:25px;
height:25px;
border-radius:8px;
box-shadow:0 0 10px #0000004d
}
.vue-flow__node-color-selector .color-selector button:hover {
box-shadow:0 0 0 2px #2563eb;
transition:box-shadow .2s
}
.vue-flow__node-color-selector .color-selector button.selected {
box-shadow:0 0 0 2px #2563eb
}
.vue-flow__node-color-selector .vue-flow__handle {
background-color:#ec4899;
height:24px;
width:8px;
border-radius:4px
}
.animated-bg-gradient {
background:linear-gradient(122deg,#6f3381,#81c7d4,#fedfe1,#fffffb);
background-size:800% 800%;
-webkit-animation:gradient 4s ease infinite;
-moz-animation:gradient 4s ease infinite;
animation:gradient 4s ease infinite
}
@-webkit-keyframes gradient {
0% {
background-position:0% 22%
}
50% {
background-position:100% 79%
}
to {
background-position:0% 22%
}
}
@-moz-keyframes gradient {
0% {
background-position:0% 22%
}
50% {
background-position:100% 79%
}
to {
background-position:0% 22%
}
}
@keyframes gradient {
0% {
background-position:0% 22%
}
50% {
background-position:100% 79%
}
to {
background-position:0% 22%
}
}五、预览效果

六、个人实现

七、问题记录
vueflow每个层级节点的位置position无法自动生成,所以需要自己进行封装。我是根据层级来进行计算从顶部依次向下布局。
<script setup lang="ts">
import {ref} from "vue";
import {tabledetail} from "./datatable.api";
import blood from "./compoent/blood.vue"
import {markertype} from "@vue-flow/core";
import { gettestlistindexbytablename } from "@/views/test/common/test.api";
import {useuserstore} from "@/store/modules/user";
const userstore = useuserstore();
// 详情抽屉
const drawerdetail = ref({})
const draweropenflag = ref(false)
const drawerdetailfields = ref([])
const nodes = ref([]);
const edges = ref([]);
/**
* 查看详情
* @param record
*/
const onhandleopendrawer = async (record) => {
const detail = await tabledetail(record)
let fields = []
for (let fieldname of object.keys(detail.fields) || []) {
fields.push(detail.fields[fieldname])
}
drawerdetail.value = detail
drawerdetailfields.value = fields
draweropenflag.value = true
nodes.value = []
edges.value = []
// 添加节点
addnode({
id: 'testyuan',
type: 'data-source',
data: { database: record.database, testyuantable: record.tablename, fieldarr: fields },
position: { x: 0, y: 90 },
});
addnode({
id: '0',
type: 'data-set',
data: { database: record.database, testyuantable: record.tablename, fieldarr: fields },
position: { x: 350, y: 70 },
});
// 添加从到集的边
addedge({ id: 'first', source: 'testyuan', target: '0', markerend: markertype.arrowclosed });
// 获取指标列表并添加节点和边
const tests = await gettestlistindexbytablename({ tablename: record.tablename });
if (tests.length > 0) {
for (let i = 0; i < tests.length; i++) {
const test = tests[i];
addnode({
id: test.id.tostring(),
type: 'index-info',
position: { x: 0, y: 0 },
data: {
indexnameen: test.indexnameen,
indexnamecn: test.indexnamecn,
group: getindexclass(test.secondlevel),
type: gettesttype(test.indextype)
},
});
addedge({
id: test.id.tostring(),
source: test.parentid,
target: test.id.tostring(),
markerend: markertype.arrowclosed,
});
}
// 记录每个 level 出现的次数
let levelcounts = {};
for (let j=2; j<nodes.value.length; j++) {
const node = nodes.value[j];
const level = number(getnodelevel(edges.value,node.id.tostring()));
node.position.x = 350 * level;
// 更新 level 出现的次数
if (levelcounts[level]) {
levelcounts[level]++;
} else {
levelcounts[level] = 1;
}
node.position.y = 100 * levelcounts[level];
}
} else {
console.warn('no tests found for table:', record.tablename);
}
}
// 封装获取当前节点层级的函数
const getnodelevel = (edges, nodeid) => {
const levelmap = {};
const visited = new set();
const dfs = (node, level) => {
visited.add(node);
levelmap[node] = level;
// 遍历边列表,找到所有从当前节点出发的边
edges.foreach((edge) => {
if (edge.source === node && !visited.has(edge.target)) {
// 递归地更新目标节点的下一层级
dfs(edge.target, level + 1);
}
});
};
// 起始节点id为"0"
dfs('0', 1);
return levelmap[nodeid] || 'node not found in the graph';
};
// 封装添加节点的函数
const addnode = (node) => {
nodes.value.push(node);
};
// 封装添加边的函数
const addedge = (edge) => {
edges.value.push(edge);
};
const firstarr = ref([]);
const tree = userstore.gettestdictalltree.find(t => t.dictcode === "first");
firstarr.value = tree ? tree.dictitemlist : [];
// 获取指标分级并转成对象
const firstmap = firstarr.value.reduce((acc, item) => {
acc[item.itemvalue] = item.itemtext;
return acc;
}, {});
const getindexclass = (indexclass) => {
return firstmap[indexclass] || '';
};
// 获取指标分类并转成对象
const testtypemap = userstore.testdictalltreeobj['testtype'].reduce((acc, item) => {
acc[item.itemvalue] = item.itemtext;
return acc;
}, {});
// 根据indextype获取指标类型
const gettesttype = (indextype) => {
return testtypemap[indextype] || '';
};
</script>
<template>
<a-drawer
v-model:open="draweropenflag"
title="详情"
width="700"
placement="right"
>
<div style="font-size: 16px; font-weight: 500; color: rgba(0, 0, 0, 0.88); margin-left: 23px">
基础信息
</div>
<blood :nodes="nodes" :edges="edges"></blood>
</a-drawer>
</template>
<style scoped lang="less"></style>以上就是vue3实现vueflow流程组件的详细指南的详细内容,更多关于vue3 vueflow流程组件的资料请关注代码网其它相关文章!
发表评论