UNPKG

flow-topology-vue3

Version:

flow-topology-vue3是一个用于创建流拓扑图的Vue 3组件库。它提供了一组可重用的组件和实用程序,以帮助您构建交互式的、具有视觉吸引力的流程图、流程图或表示数据流的任何其他类型的图

902 lines (829 loc) 20.9 kB
## flow-topology-vue3 是一个用于创建流拓扑图的 Vue 3 组件库, 导出 Topology、TemplateWrapper、AnchorWrapper 组件。 ### 示例图: ![示例](https://i-blog.csdnimg.cn/direct/b2b0b44ed2c74f74a19690eb921a50b9.jpeg) ### 留言板: [留言板](https://blog.csdn.net/qq_40040585/article/details/148478175?sharetype=blogdetail&sharerId=148478175&sharerefer=PC&sharesource=qq_40040585&spm=1011.2480.3001.8118) ### 特性 - 纯 div + svg 实现,无依赖。 - 支持拖拽节点、连线。 - 支持自定义节点样式和锚点(分支)样式。 - 模板化节点开发(通过 TemplateWrapper)。 - 支持全选、放大、缩小、截图。 - 支持锚点(分支)连接多个节点。 ### 环境 - Vue3 - Node.js 18.x+(兼容更高版本) ## 安装 ```bash npm install flow-topology-vue3 ``` ## 使用 - 全局引入 ```js <!-- main.ts --> import { createApp } from 'vue' import App from './App.vue' import FlowTopologyVue3 from 'flow-topology-vue3' import 'flow-topology-vue3/dist/flow-topology-vue3.css' const app = createApp(App) app.use(FlowTopologyVue3) app.mount('#app') ``` - 按需引入 ```js <!-- page.vue --> <template> <div class="contianer"> <Topology v-model="nodes"></Topology> <div class="quick-elements"> <TemplateWrapper v-for="item in templateNode" :key="item.id" :generator="item"> <div class="quick-element">{{ item.title }}</div> </TemplateWrapper> </div> </div> </template> <script setup> import { Topology, TemplateWrapper, AnchorWrapper } from 'flow-topology-vue3' import 'flow-topology-vue3/dist/flow-topology-vue3.css' const nodes = ref([]) </script> ``` - 自动引入 ```js npm install unplugin-vue-components -D ``` ```js // vite.config.js import { defineConfig } from 'vite' import Components from 'unplugin-vue-components/vite' import { FlowTopologyVue3Resolver } from 'flow-topology-vue3/dist/flow-topology-vue3.es.js' export default defineConfig({ plugins: [ Components({ resolvers: [FlowTopologyVue3Resolver()] }) ] }) <!-- page.vue --> <template> <div class="contianer"> <Topology v-model="nodes"></Topology> <div class="quick-elements"> <TemplateWrapper v-for="item in templateNode" :key="item.id" :generator="item"> <div class="quick-element">{{ item.title }}</div> </TemplateWrapper> </div> </div> </template> <script setup> import 'flow-topology-vue3/dist/flow-topology-vue3.css' const nodes = ref([]) </script> ``` ### 数据据类型 <table> <thead> <tr> <th>类型名称</th> <th></th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>Ftv3BranchVO</td> <td> { id: string | number, label: string, to?: (string | number)[], } </td> <td> 分支数据类型,用于定义节点的分支信息。 </td> </tr> <tr> <td>Ftv3LineVO</td> <td>{ id: string | number label: string to?: Array<number | string> } </td> <td> 连线数据类型,用于定义节点之间的连接线信息。 </td> </tr> <tr> <td>Ftv3NodeVO</td> <td> { id: string | number, position: { x: number, y: number, }, canDrag?: boolean, title?: string, content?: string, width?: number, branches: Ftv3LineVO[], [prop: string]: any, } </td> <td> 节点数据类型,用于定义节点的位置、大小、分支等信息。 </td> </tr> </tbody> </table> ``` import { Ftv3NodeVO, Ftv3BranchVO, Ftv3LineVO } from 'flow-topology-vue3' ``` ### 组件 #### Topology 主流程图组件 - 属性 <table> <thead> <tr> <th>参数</th> <th>类型</th> <th>是否必需</th> <th>默认值</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>v-model/model-value</td> <td>Array as PropType<{ id: string | number, position: { x: number, y: number, }, canDrag?: boolean, title?: string, content?: string, width?: number, branches: { id: string | number, label: string, to?: (string | number)[], }[], [prop: string]: any, }[]></td> <td></td> <td>[]</td> <td>所有节点和连线数据</td> </tr> <tr> <td>containerWidth</td> <td>[Number, String]</td> <td></td> <td>100%</td> <td>画布外容器宽度 支持数字 px % 等</td> </tr> <tr> <td>containerHeight</td> <td>[Number, String]</td> <td></td> <td>100vh</td> <td>画布外容器高度 支持数字 px % 等</td> </tr> <tr> <td>containerBackgroundColor</td> <td>String</td> <td></td> <td>#f9f9f9</td> <td>画布外容器背景</td> </tr> <tr> <td>canvasWidth</td> <td>Number </td> <td></td> <td>100vh</td> <td>画布宽度 仅支持数字</td> </tr> <tr> <td>canvasHeight</td> <td>Number </td> <td></td> <td>100vh</td> <td>画布高度 仅支持数字</td> </tr> <tr> <td>showGridPattern</td> <td>Boolean </td> <td></td> <td>true</td> <td>是否显示网格</td> </tr> <tr> <td>gridPatternColor</td> <td>String </td> <td></td> <td>#e5e5e5</td> <td>网格线颜色</td> </tr> <tr> <td>gridPatternGap</td> <td>Number </td> <td></td> <td>20</td> <td>网格大小</td> </tr> <tr> <td>showToolsBar</td> <td>Boolean </td> <td></td> <td>true</td> <td>是否显示工具栏</td> </tr> <tr> <td>toolsBarList</td> <td>Array as PropType<{ ViewCenter = 'viewCenter', ToggleSelect = 'toggleSelect', Zoom = 'zoom', Capture = 'capture' }[]></td> <td></td> <td>['viewCenter', 'toggleSelect', 'zoom', 'capture']</td> <td>工具栏种类(定位中心、全选/反选、缩放、截图)</td> </tr> <tr> <td>toolsBarPosition</td> <td>String as PropType<{ Top = 'top', TopStart = 'top-start', TopEnd = 'top-end', Bottom = 'bottom', BottomStart = 'bottom-start', BottomEnd = 'bottom-end', Left = 'left', Right = 'right' }></td> <td></td> <td>bottom-end</td> <td>工具栏位置</td> </tr> <tr> <td>toolsBarPositionOffset</td> <td>Number</td> <td></td> <td>20</td> <td>工具栏距离容器边距位置</td> </tr> <tr> <td>readOnly</td> <td>Boolean</td> <td></td> <td>false</td> <td>是否只读</td> </tr> <tr> <td>beforeDeleteLine</td> <td> Function as PropType<(line: { fromNodeId: string | number fromBranchId: string | number toNodeId: string | number }) => boolean | Promise<boolean>></td> <td></td> <td>() => true</td> <td> 删除连线前的钩子,参数是删除的线 若返回false时或者Promise.reject()阻止删除 </td> </tr> <tr> <td>beforeDeleteNode</td> <td> Function as PropType<(node: { id: string | number, position: { x: number, y: number, }, canDrag?: boolean, title?: string, content?: string, width?: number, branches: { id: string | number, label: string, to?: string | number, }[], [prop: string]: any, }) => boolean | Promise<boolean>></td> <td></td> <td>() => true</td> <td> 删除节点前的钩子,参数是删除的节点,若返回false时或者Promise.reject()阻止删除 </td> </tr> <tr> <td>beforeDeleteAllNode</td> <td> Function as PropType<() => boolean | Promise<boolean>></td> <td></td> <td>() => true</td> <td> 删除所有节点前的钩子,若返回false时或者Promise.reject()阻止删除 </td> </tr> <tr> <td>renderItem</td> <td> Function as PropType<(nodeVO: NodeVO) => any></td> <td></td> <td>null</td> <td> 自定义函数渲染节点内容,参数是节点数据对象,返回自定义渲染内容 </td> </tr> <tr> <td>captureBackgroundColor</td> <td> String </td> <td></td> <td>容器背景色</td> <td> 截图背景色, 若不设置则使用容器背景色 </td> </tr> <tr> <td>canConnectMultiLines</td> <td> Boolean </td> <td></td> <td>false</td> <td> 是否允许多条连线, 若为true则连线时一个锚点(分支)支持多个连线, 若为false则连线时一个锚点(分支)只支持一条连线 </td> </tr> </tbody> </table> - 事件 <table> <thead> <tr> <th>事件名</th> <th>参数</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>change</td> <th>{ type: 'add_node' | 'delete_node' | 'update_node_position' | 'add_line' | 'delete_line' | 'delete_node_line' , data: any}</th> <td>节点和线改变,参数为响应操作的数据,tips:删除接点和连接线请使用键盘delete键</td> </tr> <tr> <td>selectNode</td> <th>NodeVO</th> <td>选中节点,参数为选中的节点数据</td> </tr> <tr> <td>capture</td> <th>string</th> <td>截图, 参数为base64</td> </tr> </tbody> </table> - 插槽 默认插槽用于自定义渲染节点内容,可通过插槽参数获取到当前节点的数据; - 使用方法 ```html <Topology v-model="nodes" > <!-- 1、 插槽用于自定义渲染节点内容,可通过插槽参数获取到当前节点的数据; 2、 可通过renderItem渲染; 3、 插槽优先级高于renderItem; 4、 不使用插槽或者renderItem有节点内容渲染; <template #default="{ node }"> <template v-if="node.branches && node.branches.length"> <AnchorWrapper v-for="branch in node.branches" :key="branch.id" :node="node" :branch="branch"> <span>{{ branch.label }}</span> </AnchorWrapper> </template> </template> --> </Topology> </> ``` #### TemplateWrapper TemplateWrapper 组件用于包裹模板内容,并提供插槽供自定义渲染。 - 属性 - 插槽 默认插槽用于自定义渲染模板内容。 <table> <thead> <tr> <th>参数</th> <th>类型</th> <th>是否必需</th> <th>默认值</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>generator</td> <td>{ id: string | number, position: { x: number, y: number, }, canDrag?: boolean, title?: string, content?: string, width?: number, branches: { id: string | number, label: string, to?: string | number, }[], [prop: string]: any, }</td> <td></td> <td>-</td> <td>节点的数据</td> </tr> <tr> <td>disabled</td> <td>Boolean</td> <td></td> <td>false</td> <td>是否允许拖拽</td> </tr> </tbody> </table> - 使用方法 - 模版中使用 ```html <div class="quick-elements"> <TemplateWrapper v-for="item in templateNode" :key="item.id" :generator="item"> <div class="quick-element">{{ item.title }}</div> </TemplateWrapper> </div> ``` - h 函数渲染 ```js h( 'div', {...你的属性...}, templateNode.branches.map(t => { return h( TemplateWrapper, { generator: t, disabled: t.disabled, }, [ h('div', { class: 'quick-element' }, t.title) ] ) }) ) ``` #### AnchorWrapper AnchorWrapper 组件用于包裹锚点内容,并提供插槽供自定义渲染。 - 属性 <table> <thead> <tr> <th>参数</th> <th>类型</th> <th>是否必需</th> <th>默认值</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>node</td> <td>{ id: string | number, position: { x: number, y: number, }, canDrag?: boolean, title?: string, content?: string, width?: number, branches: { id: string | number, label: string, to?: string | number, }[], [prop: string]: any, }</td> <td></td> <td>-</td> <td>节点的数据</td> </tr> <tr> <td>branch</td> <td> { id: string | number label: string to?: string | number }</td> <td></td> <td></td> <td>当前节点分支</td> </tr> </tbody> </table> - 插槽 默认插槽用于自定义渲染锚点内容。 - 使用方法 - 模版中使用 ```html <AnchorWrapper v-for="branch in node.branches" :key="branch.id" :node="node" :branch="branch"> <span>{{ branch.label }}</span> </AnchorWrapper> ``` - h 函数渲染 ```js h( 'div', {...你的属性...}, node.branches.map(branch => { return h( AnchorWrapper, { class: `topo-branch ${!branch.to ? 'free' : ''}`, node: toRaw(node), branch: toRaw(branch) }, () => branch.label ) }) ) ``` ### 组件实例的方法 <table> <thead> <tr> <th>方法名</th> <th>参数</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>selectNode</td> <td>id:string | number</td> <td>选中节点</td> </tr> <tr> <td>deleteNode</td> <td>id:string | number</td> <td>删除单个节点</td> </tr> <tr> <td>deleteAllNode</td> <td>-</td> <td>删除所有节点</td> </tr> </tbody> </table> 使用示例 ```js const handleDel = () => { topologyRef.value.deleteNode('48da386a-016a-4e49-a26f-f0dfec0d3880') } const handleDelAll = () => { topologyRef.value.deleteAllNode() } const handleSelect = () => { topologyRef.value.selectNode('500e4096-78a8-465b-b0f3-e748cf5716b3') } ``` ### DEMO ```js <template> <div class="contianer"> <Topology v-model="nodes" :beforeDeleteLine="beforeDeleteLine" :beforeDeleteAllNode="onBeforeDeleteAllNode" @capture="onCapture" ref="topologyRef" > <!-- <template #default="{ node }"> {{ node }} <template v-if="node.branches && node.branches.length"> <AnchorWrapper v-for="branch in node.branches" :node="node" :branch="branch"> <span>{{ branch.label }}</span> </AnchorWrapper> </template> </template> --> </Topology> <div class="quick-elements"> <TemplateWrapper v-for="item in templateNode" :key="item.id" :generator="item"> <div class="quick-element">{{ item.title }}</div> </TemplateWrapper> </div> <div style="position: fixed; top: 0; left: 50%; transform: translateX(-50%); z-index: 1000"> <button @click="handleDel">删除节点</button> <button @click="handleDelAll">删除全部节点</button> <button @click="handleSelect">选中节点</button> </div> </div> </template> <script setup lang="ts"> import { ref, h, toRaw } from 'vue' import { Topology, TemplateWrapper, AnchorWrapper } from 'flow-topology-vue3' import 'flow-topology-vue3/dist/flow-topology-vue3.css' import { Ftv3NodeVO, Ftv3BranchVO, Ftv3LineVO } from 'flow-topology-vue3' const topologyRef = ref(null) const nodes = ref<Ftv3NodeVO[]>([ { id: 'bb80fa18-6a4a-49fd-9d33-a7f414a58b6a', title: '开始节点', score: 0, status: '待录音', content: '这是开始节点的内容', type: 1, position: { x: 4905, y: 4645 }, canDrag: false, branches: [ { id: '2c3602cc-609b-4246-a2c6-d7f19a418368', label: '默认', to: ['46ca30d5-b9cd-48e3-9814-497aab383e0d'] } ] }, { id: '46ca30d5-b9cd-48e3-9814-497aab383e0d', title: '开场白', score: 0, status: '待录音', content: '您好,请问有什么可以帮助你吗?', type: 1, position: { x: 4899, y: 4804 }, canDrag: true, branches: [ { id: '53419a2f-5141-4fb2-9134-0a0986e88268', label: '默认', to: ['500e4096-78a8-465b-b0f3-e748cf5716b3'] }, { id: 'cc9bd42c-11b0-46be-9910-5e121c6bf18c', label: '需要', to: ['500e4096-78a8-465b-b0f3-e748cf5716b3'] }, { id: '07aead90-fd4f-4a4b-a889-d191819aef9b', label: '不需要', to: ['82009f5b-28fd-4514-a52e-41293823ca0d'] } ] }, { id: '500e4096-78a8-465b-b0f3-e748cf5716b3', title: '了解退保原因', score: 0, status: '待录音', content: '我要退保,请问您是因为什么要退保呢?', type: 1, position: { x: 4662, y: 5045 }, canDrag: true, branches: [ { id: 'e1b45c8c-6d7c-4b2c-b90c-3a1e155b4b82', label: '默认', to: ['48da386a-016a-4e49-a26f-f0dfec0d3880'] } ] }, { id: '48da386a-016a-4e49-a26f-f0dfec0d3880', title: '跳转节点', score: 0, status: '待录音', content: '好的,退保', type: 1, position: { x: 4652, y: 5210 }, canDrag: true, branches: [{ id: '82009f5b-28fd-4514-a52e-41293823ca0d', label: '默认' }] }, { id: '82009f5b-28fd-4514-a52e-41293823ca0d', title: '结束节点', score: 0, status: '待录音', content: '挂断啦', type: 1, position: { x: 5029, y: 5047 }, canDrag: true, branches: null } ]) const templateNode: Array<Ftv3NodeVO> = [ { id: '', title: '开始节点', score: 0, status: '待录音', content: 'sdfasfasfasdfasdfasdfasdfasdfsadf', type: 1, position: { x: 0, y: 0 }, canDrag: true, branches: [{ id: '', label: '默认' }] }, { id: '', title: '跳转节点', score: 0, status: '待录音', content: '', type: 1, position: { x: 0, y: 0 }, canDrag: true, branches: [ { id: '', label: '默认' }, { id: '', label: '分支1' }, { id: '', label: '分支2' } ] }, { id: '', title: '结束节点', score: 0, status: '待录音', content: 'sdfasfasfasdfasdfasdfasdfasdfsadf', type: 1, position: { x: 0, y: 0 }, canDrag: true, branches: null } ] const beforeDeleteLine = (line: Ftv3LineVO) => { return new Promise((resolve, reject) => { resolve(confirm('你确定要删除这条线吗?')) }) } const beforeDeleteNode = (node: Ftv3NodeVO) => { return Promise.resolve(confirm('你确定要删除这个节点吗?')) } const onBeforeDeleteAllNode = () => { return Promise.resolve(confirm('你确定要删除所有节点吗?')) } const renderItem = (node: Ftv3NodeVO) => { return h('div', { style: 'color: red;' }, [ h('div', {}, node.title), h('div', {}, node.content), h( 'div', {}, node.branches.map(branch => { return h( AnchorWrapper, { class: `topo-branch ${!branch.to ? 'free' : ''}`, node: toRaw(node), branch: toRaw(branch) }, () => branch.label ) }) ) ]) } const handleDel = () => { topologyRef.value.deleteNode('48da386a-016a-4e49-a26f-f0dfec0d3880') } const handleDelAll = () => { topologyRef.value.deleteAllNode() } const handleSelect = () => { topologyRef.value.selectNode('500e4096-78a8-465b-b0f3-e748cf5716b3') } const onCapture = img => {} </script> <style> body { overflow: hidden; } </style> <style scoped> .contianer { overflow: hidden; } .quick-elements { position: fixed; top: 10px; left: 10px; z-index: 10; display: flex; flex-direction: column; gap: 8px; font-size: 17px; } .quick-element { padding: 8px 12px; background-color: #f0f8ff; border: 1px dashed #0066cc; border-radius: 4px; cursor: grab; user-select: none; } .quick-element:active { cursor: grabbing; } </style> ```