看如下图所示的功能,是不是可高级了?什么,你没看懂?拜托双击放大看!
是的,我最近消失了一段时间就是在研究这个玩意的实现,通过不懈努力与钻研并参考其他人员实现并加以改造,很好,终于有点小成果,这不就迫不及待给大家分享出来!使用的第三组件为VANT-X6引擎!
自己看官方文档:->https://x6.antv.antgroup.com/tutorial/getting-started
通过上述流后,可以任意组装出查询SQL语句或者是结构交给后端进行查询显示!这远比给定的那些搜索框框来的更加有性价比!用户搜索功能那就是嗖的一下提升了好多个档次。
来吧,到了最重要的环节,代码展示:
一、依赖安装
在项目的依赖包中添加以下依赖:最好按照我使用的版本添加哦,避免出现不兼容API报错无法运行!
"@antv/x6": "1.34.6", "@antv/hierarchy": "0.6.8", "@antv/x6-vue-shape": "1.3.2", "@vue/composition-api":"1.3.0"
完成后,进行npm install或yarn install,取决于你使用的是什么环境脚本!
二、页面代码:
queryGraph.vue 页面代码
<template> <div id="container" style="height: 100%;width:100%"></div> </template> <script> import { Graph } from '@antv/x6' import Hierarchy from '@antv/hierarchy' import '@antv/x6-vue-shape' import condition from './queryCondition.vue' //这是我的vue组件,作为子节点展示在思维导图上 import { findItem, lastChild, setData, addChildNode, removeNode, randomId } from './fun' export default { data() { return { graphData: { 'id': '1', 'type': 'original—add', 'width': 80, 'height': 30, "children": [ // { // "id": 0.28207584597793156, // "type": "relative", //关系节点 // "width": 44, // "height": 44, // "data": { // "relative": "and" //and并且 or或者 // }, // "children": [ // { // "id": 0.32858917851150116, // "type": "condition-text", //条件节点 // "width": 90, // "height": 44, // "level": 1, //判断它是第几级的条件节点 // "edgeText": "", // "data": { // "complete": true, // "form": {} //你的业务数据 // } // }, // { // "id": 0.30546487070416783, // "type": "vue-shape", //自定义组件 业务节点 // "width": 744, // "height": 44, // "level": 1, // "edgeText": "", // "data": { // "complete": false, // "form": {} //你的业务数据 // } // } // ] // } ] } //默认只有一个根节点 } }, mounted() { this.init() }, methods: { //初始化⽅法 init() { let self = this Graph.registerNode( 'original—add', { inherit: 'rect', width: 80, height: 30, label: '+纳入条件', attrs: { //样式代码 body: { rx: 4, ry: 4, stroke: '#037AFB', fill: '#037AFB', strokeWidth: 1, event: 'add:original' //根节点点击事件 }, label: { fontSize: 14, fill: 'white', event: 'add:original'//根节点点击事件 } } }, true, ) //表示《并且 或者》的关系节点 Graph.registerNode( 'relative', { inherit: 'rect', markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'text', selector: 'label_text' }, { tagName: 'image', selector: 'switch' } ], attrs: { //样式代码 body: { rx: 4, ry: 4, stroke: 'orange', fill: 'orange', strokeWidth: 1, event: 'change:relative' }, label_text: { fontSize: 14, fill: 'white', event: 'change:relative' }, switch: { event: 'change:relative' //关系节点 切换 关系事件 }, text: { text: '并且' } }, data: { relative: 'and' } //and并且 or或者 默认为 并且 } ) //自定义vue 业务节点 Graph.registerVueComponent('condition', condition, true) //显示条件语句 Graph.registerNode('condition-text', { inherit: 'rect', markup: [ { tagName: 'rect', selector: 'body' }, { tagName: 'g', attrs: { class: 'content' }, children: [] } ], attrs: {}//样式代码 } ) // 弯的边 Graph.registerEdge( 'mindmap-edge', { inherit: 'edge', router: { name: 'manhattan', args: { startDirections: ['right'], endDirections: ['left'] } }, connector: { name: 'rounded' }, attrs: { line: { targetMarker: '', stroke: '#A2B1C3', strokeWidth: 2 } }, //样式代码 zIndex: 0 }, true, ) // 直的边 Graph.registerEdge( 'straight-edge', { inherit: 'edge', attrs: {}, //样式代码 zIndex: 0 }, true, ) //编辑 Graph.registerNodeTool('edit', { inherit: 'button', // 基类名称,使用已经注册的工具名称。 markup: [ { tagName: 'rect', selector: 'button', attrs: { fill: '#296FFF', cursor: 'pointer', width: 32, height: 28 } }, { tagName: 'image', selector: 'icon', attrs: { 'xlink:href': 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SYCuQ6HHs5cAAAAAAAAAAAAAARQnAQ', cursor: 'pointer', width: 16, height: 16, x: 8, y: 6 } } ], x: '100%', y: '100%', offset: { x: -96, y: -72 }, onClick({ cell }) { const dataItem = cell.getData() setData(this.graphData, cell.id, { ...dataItem, complete: false, isEdit: true }) cell.setData({ ...dataItem, complete: false, isEdit: true }) //打开编辑时,子级元素偏移 const firstChild = cell.getChildAt(0) if (firstChild) { const cellWidth = dataItem.form.unit ? 844 : 744 const x = cellWidth - firstChild.position({ relative: true }).x + 80 //编辑框 - 第一个子级位置 - 连接线宽 = 子级偏移量 cell.getChildAt(0).translate(x) } } }) //删除 Graph.registerNodeTool('del', { inherit: 'button', // 基类名称,使用已经注册的工具名称。 markup: [ { tagName: 'rect', selector: 'button', attrs: { fill: '#296FFF', cursor: 'pointer', width: 32, height: 28 } }, { tagName: 'image', selector: 'icon', attrs: { 'xlink:href': 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SYCuQ6HHs5cAAAAAAAAAAAAAARQnAQ', cursor: 'pointer', width: 16, height: 16, x: 8, y: 6 } } ], x: '100%', y: '100%', offset: { x: -64, y: -72 }, onClick({ cell }) { if (removeNode(cell.id, this.graphData)) { render(graph, this.graphData) } } }) //新增限定条件 Graph.registerNodeTool('add-condition', { inherit: 'button', // 基类名称,使用已经注册的工具名称。 markup: [ { tagName: 'rect', selector: 'button', attrs: { fill: '#296FFF', cursor: 'pointer', width: 32, height: 28 } }, { tagName: 'image', selector: 'icon', attrs: { 'xlink:href': 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SYCuQ6HHs5cAAAAAAAAAAAAAARQnAQ', cursor: 'pointer', width: 16, height: 16, x: 8, y: 6 } } ], x: '100%', y: '100%', offset: { x: -32, y: -72 }, onClick({ cell }) { debugger const { id } = cell const dataItem = findItem(this.graphData, id).node const lastNode = lastChild(dataItem)//找到当前node的最后一级,添加 if (addChildNode(lastNode.id, '并且', graphData)) render(graph, this.graphData) } }) //关系节点 点击增加条件事件 Graph.registerNodeTool('relative:add-condition', { inherit: 'button', // 基类名称,使用已经注册的工具名称。 markup: [ { tagName: 'rect', selector: 'button', attrs: { fill: '#296FFF', cursor: 'pointer', width: 32, height: 28 } }, { tagName: 'image', selector: 'icon', attrs: { 'xlink:href': 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SYCuQ6HHs5cAAAAAAAAAAAAAARQnAQ', cursor: 'pointer', width: 16, height: 16, x: 8, y: 6 } } ], x: '100%', y: '100%', offset: { x: -32, y: -72 }, onClick({ cell }) { debugger const { id } = cell if (addChildNode(id, '', this.graphData)) render(graph, this.graphData) } }) //边增加条件 Graph.registerEdgeTool('edge:add-condition', { inherit: 'button', // 基类名称,使用已经注册的工具名称。 markup: [ { tagName: 'rect', selector: 'button', attrs: { fill: '#296FFF', cursor: 'pointer', fontSize: 16, width: 20, height: 20, rx: 2, ry: 2, stroke: '#296FFF', strokeWidth: 1 } }, { tagName: 'text', selector: 'label', textContent: '+', attrs: { x: 5, y: 15, fontSize: 16, cursor: 'pointer', fill: '#ffff' } } ], distance: '100%', offset: { y: -10, x: -10 }, onClick({ cell }) { const { node, parent } = findItem(self.graphData, cell.target.cell) const newId = randomId() const childP = { children: [node], id: newId, type: 'relative', width: 40, height: 40, level: 2, data: { relative: 'and', type: 'document' } } const currentIndex = parent.children.findIndex(item => item.id === node.id) parent.children[currentIndex] = childP let anode = addChildNode(newId, '', self.graphData) anode.width = 550 if (anode) { render(graph, self.graphData) } // const { node, parent } = findItem(self.graphData, cell.target.cell) // const newId = randomId() // const childP = { // id: newId, // type: "vue-shape", //自定义组件 业务节点 // width: 550, // height: 44, // level: 1, // edgeText: "", // data: { // complete: false, // form: {} //你的业务数据 // } // } // parent.children.push(childP) // render(graph, self.graphData) } }) let graph = new Graph({ background: { color: '#fff' }, container: document.getElementById('container'), panning: { enabled: true }, selecting: { enabled: true }, keyboard: { enabled: true }, grid: true, mousewheel: { enabled: true, modifiers: ['ctrl', 'meta'] }, interacting: { nodeMovable: false } }) const render = (graph, graphData) => { const result = Hierarchy.mindmap(graphData, { direction: 'H', getHeight(d) { return d.height }, getWidth(d) { return d.width }, getHGap() { return 40 }, getVGap() { return 20 }, getSide: () => { return 'right' } }) const cells = [] const traverse = (hierarchyItem, parentId) => { if (hierarchyItem) { const { data, children } = hierarchyItem const node = graph.createNode({ ...data, shape: data.type, x: hierarchyItem.x, y: hierarchyItem.y, component: 'condition' }) if (parentId) { //有父级则插入父级 const parent = graph.getCellById(parentId) parent && parent.addChild(node) } if (data.type === 'condition-text') { //条件文案节点 根据文字长度,计算宽度,这边粗糙了点,将数字也按中文字长度计算,可优化 //下面是我的根据我的业务数据结构计算长度,可参考 //const { key, opt, value = [], unit } = data.data.form //const keyText = key.displayText //const optText = opt.displayText //const valueText = typeof value === 'string' ? value : value.join(',') //const unitText = valueText.length ? (unit || '') : '' //const width = (keyText.length + optText.length + valueText.length + unitText.length) * 16 + 10 //node.attr('key/text', `${keyText},`) //node.attr('opt', { text: `${optText} `, x: keyText.length * 16 + 5 }) //node.attr('value', { text: valueText, x: (keyText.length + optText.length) * 16 + 5 }) //node.attr('unit', { text: unitText, x: (keyText.length + optText.length + valueText.length) * 16 + 5 }) //node.resize(width, 44) //data.width = width } //关系节点,默认是并且为蓝色,是或者的话,需要切换颜色判断 if (data.type === 'relative' && data.data.relative === 'or') { node.setAttrs({ body: { stroke: '#CEE8D9', fill: '#CEE8D9' }, label_text: { fill: '#008451' }, switch: { 'xlink:href': "" }, text: { text: '或者' } }) } cells.push(node) //子节点边 if (children) { children.forEach((item) => { const { id, data: itemData } = item cells.push( graph.createEdge({ shape: itemData.edgeText ? 'straight-edge' : 'mindmap-edge', source: { cell: hierarchyItem.id, anchor: { name: itemData.type === 'topic-child' ? 'right' : 'center', args: { dx: itemData.type === 'topic-child' ? -16 : '25%' } } }, target: { cell: id, anchor: { name: 'left' } }, labels: [{ attrs: { text: { text: itemData.edgeText || '' } } }] }), ) traverse(item, node.id) }) } } } traverse(result) graph.resetCells(cells) // graph.scaleContentToFit({ maxScale: 1 }) graph.centerContent() } //根结点添加 graph.on('add:original', ({ node }) => { debugger if (this.graphData.children.length == 0) { const { id } = node let anode = addChildNode(id, '', this.graphData) anode.id = randomId() anode.type = "vue-shape" //自定义组件 业务节点 anode.width = 550 anode.height = 44 anode.level = 1 anode.edgeText = "" anode.data = { complete: false, form: {} //你的业务数据 } anode.children = [] if (anode) { render(graph, this.graphData) } } else if (this.graphData.children.lastObject().type != 'relative') { const { id } = node let tlist = this.graphData.children this.graphData.children = [] let anode = addChildNode(id, '', this.graphData) anode.type = "relative" anode.width = 40; anode.height = 40; anode.level = 1; anode.data = { "relative": "and" //and并且 or或者 } let xlist = [] tlist.forEach(element => { xlist.push(element) }); xlist.push({ "id": randomId(), "type": "vue-shape", //自定义组件 业务节点 "width": 550, "height": 44, "level": 1, "edgeText": "", "data": { "complete": false, "form": {} //你的业务数据 } }) anode.children = xlist if (anode) { render(graph, this.graphData) } } else { const { id } = node let tlist = this.graphData.children this.graphData.children = [] let anode = addChildNode(id, '', this.graphData) anode.type = "relative" anode.width = 40; anode.height = 40; anode.level = 1; anode.data = { "relative": "and" //and并且 or或者 } let xlist = [] tlist.forEach(x=>{ xlist.push(x) }) xlist.push({ "id": randomId(), "type": "vue-shape", //自定义组件 业务节点 "width": 550, "height": 44, "level": 1, "edgeText": "", "data": { "complete": false, "form": {} //你的业务数据 } }) anode.children = xlist // tlist.push(anode) this.graphData.children = [anode] if (anode) { render(graph, this.graphData) } } }) //节点数据变化 graph.on('node:change:data', (cell) => { debugger }) //关系节点 切换《并且或者》 graph.on('change:relative', (cell) => { let node = cell.node if (node.data.relative == "and") { node.data.relative = "or" node.setAttrs({ body: { stroke: '#d4eade', fill: '#d4eade' }, label_text: { fontSize: 14, fill: '#3e845e', }, text: { text: '或者' } }) } else { node.data.relative = "and" node.setAttrs({ body: { stroke: 'orange', fill: 'orange' }, label_text: { fontSize: 14, fill: 'white', }, text: { text: '并且' } }) } debugger const dataItem = node.getData() setData(self.graphData,node.id,dataItem) debugger }) //节点聚焦 增加工具栏目 graph.on('node:mouseenter', ({ node }) => { // if (['condition-text', 'relative'].includes(node.shape)) { // if (!this.isExistUnComplete()) { //判断当前是否有未填写完成的vue组件节点 // if (node.shape === 'condition-text') { // node.setAttrs({ body: { fill: '#E9F0FF', stroke: '#296FFF' } }) // } // this.addTool(node) // } // } }) //节点失焦 移除工具栏 graph.on('node:mouseleave', ({ node }) => { // if (['condition-text', 'relative'].includes(node.shape)) { // if (node.shape === 'condition-text') { // node.setAttrs({ body: { stroke: '#CCC', fill: '#fff' } }) // } // this.removeTool(node) // } }) //边 悬浮事件 graph.on('edge:mouseenter', ({ edge }) => { //不是 根结点下第一个关系节点 并且 没有未完成的节点 可添加 const targetNode = graph.getCellById(edge.target.cell) const targetNodeData = findItem(this.graphData, edge.target.cell).node const isChild = targetNodeData.level ? targetNodeData.level === 1 : true //不是限定节点 可添加 if (!(edge.source.cell === '1' && targetNode.shape === 'relative') && isChild && !this.isExistUnComplete()) { edge.addTools(['edge:add-condition']) } }) //边 失焦 graph.on('edge:mouseleave', ({ edge }) => { if (!this.isExistUnComplete()) {//判断当前是否有未填写完成的vue组件节点 edge.removeTools(['edge:add-condition']) } }) render(graph, this.graphData) }, isExistUnComplete() { return false } } } </script> <style lang="scss"> .topic-image { visibility: hidden; cursor: pointer; } .x6-node:hover .topic-image { visibility: visible; } .x6-node-selected rect { stroke-width: 2px; } </style>
三、自定义条件组件queryCondition.vue
<template> <div class="condition"> <el-form ref="form" :model="form" label-width="0" inline> <el-row :gutter="10"> <el-col :span=8> <el-form-item class="w-100"> <el-input v-model="form.name" placeholder="搜索项目"></el-input> </el-form-item> </el-col> <el-col :span=4> <el-form-item class="w-100"> <el-select v-model="form.condition" placeholder="关系"> <el-option v-for="item in optionsList" :key="item.label" :label="item.label" :value="item.value"> </el-option> </el-select> </el-form-item> </el-col> <el-col :span=7> <el-form-item class="w-100"> <el-input v-model="form.text" placeholder="对比值"></el-input> </el-form-item> </el-col> <el-col :span=5> <el-from-item class="w-100"> <div class="flex-row w-100"> <el-button>取消</el-button> <el-button type="primary" @click="onSubmit">确定</el-button> </div> </el-from-item> </el-col> </el-row> </el-form> </div> </template> <script> // import { elForm, elFormItem, elInput, elSelect, elOption } from 'element-ui'//在这需要再次按需引入对应组件 export default { name: 'queryCondition', inject: ["getGraph", "getNode"], // components: { elForm, elFormItem, elInput, elSelect, elOption }, data() { return { form: { name:null, condition:null, text:null }, optionsList: [ { label: '等于', value: '=' }, { label: '不等于', value: '!=' }, { label: '大于', value: '>' }, { label: '大于等于', value: '>=' }, { label: '小于', value: '<' }, { label: '小于等于', value: '<=' } ] } }, mounted() { }, methods: { onSubmit(){} } } </script> <style lang="scss" scoped> .condition { padding: 0px 10px; height: 100%; background: #EFF4FF; border: 1px solid #5F95FF; border-radius: 6px; display: flex; flex-direction: row; justify-content: center; align-items: center; } .flex-row{ display: flex; flex-direction: row; justify-content: center; align-items: center; } ::v-deep { .el-form-item--small { margin: 0px; vertical-align: middle !important; } .el-button--small{ padding-left:10px; padding-right: 10px; } } </style>
四、公共方法 fun.js
import {snowFlakeId} from '@/utils/snowFlake' //查找节点的父节点 当前节点,顶级节点的数据 export const findItem = (obj, id, levelTop) => { const topNode = levelTop ? levelTop : obj.level && obj.level === 1 ? obj : null; if (obj.id === id) { return { parent: null, node: obj, topNode, }; } const { children } = obj; if (children) { for (let i = 0, len = children.length; i < len; i++) { const res = findItem(children[i], id, topNode); if (res) { return { parent: res.parent || obj, node: res.node, topNode: res.topNode, }; } } } return null; }; //查找最末级 export const lastChild = (obj) => { if (obj.children && obj.children.length) { return lastChild(obj.children[0]); } else { return obj; } }; //设置某个节点的data export const setData = (obj, id, dataItem) => { if (obj.id === id) { obj.data = dataItem; if (["vue-shape", "condition-text"].includes(obj.type)) { obj.type = dataItem.complete ? "condition-text" : "vue-shape"; } return; } if (obj.children) { obj.children.forEach((child) => { setData(child, id, dataItem); }); } }; //插入节点 export const addChildNode = (id, edgeText, data) => { const res = findItem(data, id); const dataItem = res.node; if (dataItem) { const item = { id: randomId(), type: "vue-shape", width: 744, height: 44, //内容宽高 + padding20 + 边框4 level: dataItem.level === 1 ? dataItem.level + 1 : 1, edgeText, }; if (dataItem.children) { dataItem.children.push(item); } else { dataItem.children = [item]; } return item; } return null; }; //移除节点 export const removeNode = (id, data) => { const res = findItem(data, id); const dataItem = res.parent; if (dataItem && dataItem.children) { const { children } = dataItem; const index = children.findIndex((item) => item.id === id); children.splice(index, 1); //删除当前 if (children.length && children.length < 2) { //并且或者 只有一个子级时 删除并且或者节点 const p2 = findItem(data, dataItem.id).parent; //父级的父级 const p2OtherChildren = p2.children.filter( (item) => item.id !== dataItem.id ); p2.children = [...p2OtherChildren, ...children]; } return true; } return null; }; export const randomId = ()=> { return snowFlakeId() };
目前只实现初步的效果,后期实现相关功能后再视具体是否可开放源码进行共享!
创作不易,谢谢你的点赞和关注收藏!