一、前言
使用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流程组件的资料请关注代码网其它相关文章!
发表评论