一、 首先?antv x6 分為兩個版本? 低版本和高版本
? 我這里是使用的2.0版本 并且搭配了相關插件 例如:畫布的圖形變換、地圖等
? 個人推薦 2.0版本,高版本配置多,可使用相關插件多,但是文檔描述小,仍在更新, 低版本文檔描述清晰,但是相關插件少
二、antv x6 支持自定義節點!?
? ? ? ? ? 這里要特別注意? 雖然支持自定義節點,但是連線,連線樁也自然只能節點之間互連,所以你看我的例子中,想要列表里的子節點也可以實現 互相連接,但是這是自定義節點無法做到的。
? ? ? ? ? 因為此時這一整個盒子就是 一個節點!
三、事件集合
// 事件集合loadEvents(containerRef) {// 節點雙擊this.graph.on('node:dblclick', ({ node }) => {const data = node.store.data;console.log(data);this.$router.push({path: '/modeling/homeModeling',query: {id: data.modelingId,name: data.name,layerTypeId: data.layerTypeId,tableType: data.modelingType,},});});// 連線雙擊this.graph.on('edge:dblclick', ({ edge }) => {// const data = edge.store.data;// const { type, id } = data;// alert('連線雙擊');// console.log('edge:dbclick', edge);// if (type === 'taskNode') {// this.nodeId = id;// this.showRight = true;// } else {// this.nodeId = '';// this.showRight = false;// }});// 節點鼠標移入this.graph.on('node:mouseenter',FunctionExt.debounce(({ node }) => {// 添加刪除// const x = node.store.data.size.width - 10;// node.addTools({// name: 'button-remove',// args: {// x: 0,// y: 0,// offset: { x, y: 15 },// },// });}),500,);this.graph.on('node:port-contextmenu', ({ e }) => {// console.log(// 'ports',// e,// e.currentTarget.parentElement.getAttribute('port'),// );});// 連接線鼠標移入this.graph.on('edge:mouseenter', ({ edge }) => {// edge.addTools([// 'source-arrowhead',// 'target-arrowhead',// {// name: 'button-remove',// args: {// distance: '50%',// },// },// ]);});// 節點鼠標移出this.graph.on('node:mouseleave', ({ node }) => {// // 移除刪除// node.removeTools();});this.graph.on('edge:mouseleave', ({ edge }) => {// edge.removeTools();});this.graph.on('edge:connected', ({ isNew, edge }) => {// console.log('connected', edge.source, edge.target);// if (isNew) {// // 對新創建的邊進行插入數據庫等持久化操作// }});},
四、畫布初始化
graphInit() {// 容器生成圖表const containerRef = this.$refs.containerRef;const graph = new Graph({container: containerRef,background: {color: '#F1F6F9',},grid: {size: 10, // 網格大小 10pxvisible: true, // 繪制網格,默認繪制 dot 類型網格type: 'fixedDot',args: {color: '#AFB0B1', // 網點顏色thickness: 1, // 網點大小},},panning: true, // 畫布拖拽history: true, // 啟動歷史記錄selecting: {// 選擇與框選enabled: true,rubberband: true,movable: true,strict: true,showNodeSelectionBox: true, // 顯示節點的選擇框(才能進行移動)modifiers: ['alt'],},// Scroller 使畫布具備滾動、平移、居中、縮放等能力scroller: {enabled: true,pageVisible: true,pageBreak: true,pannable: true,},// 鼠標滾輪的默認行為是滾動頁面 使用ctrl+滾輪 實現縮放mousewheel: {enabled: true,modifiers: ['ctrl', 'meta'], // +按鍵為縮放minScale: 0.5,maxScale: 2,},snapline: true, // 對齊線// 節點連接connecting: {router: {name: 'er',args: {offset: 25,direction: 'H',},},snap: true, // 自動吸附allowBlank: false, // 是否允許連接到畫布空白位置的點allowLoop: false, // 是否允許創建循環連線,即邊的起始節點和終止節點為同一節點allowNode: false, // 是否允許邊鏈接到節點(非節點上的鏈接樁)createEdge() {return new Shape.Edge({attrs: {line: {stroke: '#1684FC',strokeWidth: 2,},},});},},// 連接樁樣式 -- 高亮highlighting: {magnetAvailable: {name: 'stroke',args: {padding: 4,attrs: {strokeWidth: 4,stroke: '#1684FC',},},},},});// 小地圖const minimapContainer = this.$refs.minimapContainer;graph.use(new MiniMap({container: minimapContainer,width: '250',height: '150',scalable: true, // 是否可縮放minScale: 0.01,maxScale: 16,}),);// 圖形graph.use(new Transform({enabled: true,resizing: map,}),);// 縮放畫布內容,使畫布內容充滿視口graph.zoomToFit({ padding: 10, maxScale: 1 });// 賦值生成this.graph = graph;// 事件集合this.loadEvents(containerRef);},
五、創建Vue自定義節點
<template><divref="node_dom"class="node_warp":style="{width: node.size.width + 'px',height: node.size.height + 'px',borderTopColor: color,}"><div class="head_top" :style="{ backgroundColor }"><svg-icon :icon-class="icon" :style="{ color }"></svg-icon><div class="code_warp"><span class="code ellipsis_text">{{ node.code }}</span><span class="name ellipsis_text">{{ node.name }}</span></div><el-popoverref="popoverDom"placement="bottom-end"width="60":value="popShow"trigger="click"popper-class="filter_column_popover"@hide="popShow = false"@show="popShow = true"><svg-iconslot="reference"class="icon"type="primary"size="mini"style="opacity: 0.5;"icon-class="table_column_settings"></svg-icon><p class="header_wrap_filter_column"><el-checkboxv-model="checkAll":indeterminate="isIndeterminate"@change="handleCheckAllChange">全選</el-checkbox><!-- --><!-- <el-button size="mini" type="text" @click="resetColumn">重置</el-button> --></p><el-checkbox-groupv-model="checkList"@change="handleCheckedCitiesChange"><el-checkbox v-for="item in checkData" :key="item" :label="item">{{ item }}</el-checkbox></el-checkbox-group><div v-if="!checkData.length" class="empy">暫無數據</div></el-popover></div><div class="main"><divv-for="(item, index) in node.columnVersions":key="index"class="text "><svg-icon v-if="item.isPrimaryKey" icon-class="key"></svg-icon><span v-show="checkList.includes('英文名稱')" class="ellipsis_text">{{ item.code }}</span><div v-show="checkList.includes('字段類型')" class="type ellipsis_text">{{ item.dataType }}</div><span v-show="checkList.includes('中文名稱')" class="ellipsis_text">{{ item.name }}</span></div><divv-if="!node.columnVersions || !node.columnVersions.length"class="empy flex">暫無數據</div></div><div class="footer">{{ `共${node.columnSize || 0}個字段` }}</div></div> </template><script> import { manage } from './config'; const cityOptions = ['英文名稱', '字段類型', '中文名稱']; export default {name: 'Node',inject: ['getNode'],data() {return {num: 0,icon: '',color: '',node: {},popShow: false,checkAll: false,checkList: ['英文名稱', '字段類型'],checkData: cityOptions,isIndeterminate: true,backgroundColor: null,typeMap: manage.typeMap,};},watch: {checkList(val) {console.log(val);},},created() {const node = this.getNode();const typeMap = this.typeMap;this.node = node.store.data;const type = this.node.modelingType;this.icon = typeMap[type].icon;this.color = typeMap[type].color;this.backgroundColor = typeMap[type].backgroundColor;},methods: {handleCheckAllChange(val) {this.checkList = val ? cityOptions : [];this.isIndeterminate = false;},handleCheckedCitiesChange(value) {const checkedCount = value.length;this.checkAll = checkedCount === this.checkData.length;this.isIndeterminate =checkedCount > 0 && checkedCount < this.checkData.length;},resetColumn() {this.checkList = ['英文名稱', '字段類型'];},}, }; </script><style lang="scss" scoped> .node_warp {display: flex;border-radius: 4px;flex-direction: column;border: 1px solid #d9dae2;border-top: 5px solid #d9dae2;position: relative;user-select: none;transition: all 0.4s ease-in 0.2s;transition: width 0.25s;-webkit-transition: width 0.25s;-moz-transition: width 0.25s;-webkit-transition: width 0.25s;-o-transition: width 0.25s;.head_top {width: 100%;height: 48px;display: flex;padding-left: 10px;align-items: center;position: relative;border-bottom: 1px solid #d9dae2;.code_warp {width: 85%;font-size: 12px;margin-left: 8px;display: flex;flex-direction: column;.code {color: black;font-weight: 700;}.name {color: #b3b2bf;font-weight: 600;}}.icon {position: absolute;right: 5px;bottom: 5px;}}.main {flex: 1;width: 100%;overflow: auto;padding-right: 2px;background: #fff;.text {height: 32px;display: flex;gap: 1px;font-size: 13px;position: relative;padding-left: 20px;align-items: center;svg {position: absolute;left: 4px;top: 10px;}.type {flex: 1;height: 24px;font-size: 12px;line-height: 24px;text-align: center;border-radius: 4px;margin-right: 5px;display: inline-block;background-color: #f7f7f9;}span {flex: 1;text-align: center;}&:hover {background: #f8f8fa;}}}.footer {height: 20px;font-size: 12px;line-height: 20px;padding-left: 10px;color: rgb(156, 160, 184);border-top: 1px solid #d9dae2;background: rgb(247, 247, 249);}.ellipsis_text {white-space: nowrap;overflow: hidden;text-overflow: ellipsis;word-break: break-all;line-height: 18px;}.empy {color: #ccc;font-size: 14px;margin: 10px auto;width: fit-content;}.flex {display: flex;height: calc(100% - 30px);align-items: center;} } </style>
六、注冊引入Vue自定義節點
1、安裝依賴
? ? ??"@antv/x6-vue-shape": "2.0.6",
? ? ? yarn add?antv/x6-vue-shape@2.0.6
2、引入 Vue 自定義組件
? ? ??import CustomNode from '../node';
3、引入插件的方法
? ? ??import { register } from '@antv/x6-vue-shape'; // vue節點
4、注冊節點
????????
register({
? shape: 'custom-vue-node',
? component: CustomNode,
});
import CustomNode from '../node'; import { register } from '@antv/x6-vue-shape'; // vue節點// 注冊 Vue component register({shape: 'custom-vue-node',component: CustomNode, });
七、創建節點、創建連線、渲染節點
// 連接線 const lineNewData = newData.map((item, index) => {return {id: String(new Date().getTime() + index),shape: 'edge',// 連接源source: {cell: item.sourceTableId,},// 連接目標target: {cell: item.targetTableId,},attrs: {line: {stroke: '#1684FC',strokeWidth: 2,},},// 名字labels: [{attrs: {label: {text: item.name || '',},},},],zIndex: 0,};});// 節點const nodeData = result.map(item => {return {...item,id: item.modelingVersionId,width: Number(item.width || 300),height: Number(item.heigh || 270),// 節點類型shape: item.shape || 'custom-vue-node',position: {x: Number(item.posX || this.getRandomInt()),y: Number(item.posY || this.getRandomInt()),},};});this.erData = [...nodeData, ...lineNewData];
? 通過數據 渲染節點
watch: {data(val) {const cells = [];this.data.forEach(item => {console.log(item, item.shape);if (item.shape === 'edge') {cells.push(this.graph.createEdge(item)); // 創建連線} else {cells.push(this.graph.createNode(item)); // 創建節點}});// 清空畫布并添加用指定的節點/邊this.graph.resetCells(cells);},},
八、canvas主頁面 全部代碼
<template><div id="container" class="antv-x6"><div ref="minimapContainer" class="app-mini"></div><div ref="containerRef" class="app-content"></div><div class="operating"><el-selectv-model="value"clearablefilterableplaceholder="請選擇"size="mini":popper-append-to-body="false":class="isShow ? 'showSelect' : 'hideSelect'"@change="valChange"><el-optionv-for="item in data.filter(i => i.modelingType)":key="item.id":label="item.code":value="item.id"><div class="head_top"><svg-icon:icon-class="typeMap[item.modelingType].icon":style="{ color: typeMap[item.modelingType].color }"/><div class="code_warp"><span class="code ellipsis_text">{{ item.code }}</span><span class="name ellipsis_text">{{ item.name }}</span></div></div></el-option></el-select><div class="icon_oper"><el-tooltipclass="item"effect="dark"content="搜索"placement="bottom"><svg-icon icon-class="search_canvas" @click="search" /></el-tooltip><el-tooltipclass="item"effect="dark"content="放大"placement="bottom"><svg-icon icon-class="amplify_canvas" @click="zoomInFn" /></el-tooltip><el-tooltipclass="item"effect="dark"content="縮小"placement="bottom"><svg-icon icon-class="reduce_canvas" @click="zoomOutFn" /></el-tooltip><el-tooltipclass="item"effect="dark"content="還原"placement="bottom"><svg-icon icon-class="1_1_canvas" @click="resetFn" /></el-tooltip><el-tooltipclass="item"effect="dark"content="保存"placement="bottom"><svg-icon icon-class="saveModel" @click="submit" /></el-tooltip><el-tooltipclass="item"effect="dark":content="isFullScreen ? '退出全屏' : '全屏'"placement="bottom"><svg-icon icon-class="screen" @click="fullScreen" /></el-tooltip><el-tooltipclass="item"effect="dark"content="刷新"placement="bottom"><svg-icon icon-class="refresh" @click="redoFn" /></el-tooltip></div></div></div>
</template><script>
import { manage } from '../config';
import CustomNode from '../node';
import { Graph, Shape, FunctionExt } from '@antv/x6';
import { register } from '@antv/x6-vue-shape'; // vue節點
import { MiniMap } from '@antv/x6-plugin-minimap'; // 地圖
import { Transform } from '@antv/x6-plugin-transform'; // 圖形變換
// import { Scroller } from '@antv/x6-plugin-scroller'; // 滾動畫布const map = {enabled: true,minWidth: 200,maxWidth: 700,minHeight: 100,maxHeight: 500,orthogonal: false,restrict: false,preserveAspectRatio: false,
};// 注冊 Vue component
register({shape: 'custom-vue-node',component: CustomNode,
});export default {name: 'Er',props: {data: {type: Array,default: () => [],},},data() {return {value: '',graph: null,isShow: false,showRight: false,isFullScreen: false,typeMap: manage.typeMap,};},watch: {data(val) {const cells = [];this.data.forEach(item => {console.log(item, item.shape);if (item.shape === 'edge') {cells.push(this.graph.createEdge(item)); // 創建連線} else {cells.push(this.graph.createNode(item)); // 創建節點}});// 清空畫布并添加用指定的節點/邊this.graph.resetCells(cells);},},mounted() {this.graphInit();},methods: {graphInit() {// 容器生成圖表const containerRef = this.$refs.containerRef;const graph = new Graph({container: containerRef,background: {color: '#F1F6F9',},grid: {size: 10, // 網格大小 10pxvisible: true, // 繪制網格,默認繪制 dot 類型網格type: 'fixedDot',args: {color: '#AFB0B1', // 網點顏色thickness: 1, // 網點大小},},panning: true, // 畫布拖拽history: true, // 啟動歷史記錄selecting: {// 選擇與框選enabled: true,rubberband: true,movable: true,strict: true,showNodeSelectionBox: true, // 顯示節點的選擇框(才能進行移動)modifiers: ['alt'],},// Scroller 使畫布具備滾動、平移、居中、縮放等能力scroller: {enabled: true,pageVisible: true,pageBreak: true,pannable: true,},// 鼠標滾輪的默認行為是滾動頁面 使用ctrl+滾輪 實現縮放mousewheel: {enabled: true,modifiers: ['ctrl', 'meta'], // +按鍵為縮放minScale: 0.5,maxScale: 2,},snapline: true, // 對齊線// 節點連接connecting: {router: {name: 'er',args: {offset: 25,direction: 'H',},},snap: true, // 自動吸附allowBlank: false, // 是否允許連接到畫布空白位置的點allowLoop: false, // 是否允許創建循環連線,即邊的起始節點和終止節點為同一節點allowNode: false, // 是否允許邊鏈接到節點(非節點上的鏈接樁)createEdge() {return new Shape.Edge({attrs: {line: {stroke: '#1684FC',strokeWidth: 2,},},});},},// 連接樁樣式 -- 高亮highlighting: {magnetAvailable: {name: 'stroke',args: {padding: 4,attrs: {strokeWidth: 4,stroke: '#1684FC',},},},},});// 小地圖const minimapContainer = this.$refs.minimapContainer;graph.use(new MiniMap({container: minimapContainer,width: '250',height: '150',scalable: true, // 是否可縮放minScale: 0.01,maxScale: 16,}),);// 圖形graph.use(new Transform({enabled: true,resizing: map,}),);// 縮放畫布內容,使畫布內容充滿視口graph.zoomToFit({ padding: 10, maxScale: 1 });// 賦值生成this.graph = graph;// 事件集合this.loadEvents(containerRef);},// 事件集合loadEvents(containerRef) {// 節點雙擊this.graph.on('node:dblclick', ({ node }) => {const data = node.store.data;console.log(data);this.$router.push({path: '/modeling/homeModeling',query: {id: data.modelingId,name: data.name,layerTypeId: data.layerTypeId,tableType: data.modelingType,},});});// 連線雙擊this.graph.on('edge:dblclick', ({ edge }) => {// const data = edge.store.data;// const { type, id } = data;// alert('連線雙擊');// console.log('edge:dbclick', edge);// if (type === 'taskNode') {// this.nodeId = id;// this.showRight = true;// } else {// this.nodeId = '';// this.showRight = false;// }});// 節點鼠標移入this.graph.on('node:mouseenter',FunctionExt.debounce(({ node }) => {// 添加刪除// const x = node.store.data.size.width - 10;// node.addTools({// name: 'button-remove',// args: {// x: 0,// y: 0,// offset: { x, y: 15 },// },// });}),500,);this.graph.on('node:port-contextmenu', ({ e }) => {// console.log(// 'ports',// e,// e.currentTarget.parentElement.getAttribute('port'),// );});// 連接線鼠標移入this.graph.on('edge:mouseenter', ({ edge }) => {// edge.addTools([// 'source-arrowhead',// 'target-arrowhead',// {// name: 'button-remove',// args: {// distance: '50%',// },// },// ]);});// 節點鼠標移出this.graph.on('node:mouseleave', ({ node }) => {// // 移除刪除// node.removeTools();});this.graph.on('edge:mouseleave', ({ edge }) => {// edge.removeTools();});this.graph.on('edge:connected', ({ isNew, edge }) => {// console.log('connected', edge.source, edge.target);// if (isNew) {// // 對新創建的邊進行插入數據庫等持久化操作// }});},// 放大zoomInFn() {this.graph.zoom(0.1);},// 縮小zoomOutFn() {const Num = Number(this.graph.zoom().toFixed(1));if (Num > 0.1) {this.graph.zoom(-0.1);}},// 重置1:1resetFn() {this.graph.centerContent();this.graph.zoomTo(1); // 縮放畫布到指定的比例},// 刷新redoFn() {this.$emit('detailsEr');},// 全屏fullScreen() {// const element = document.documentElement;const element = document.getElementById('container');// 判斷是否已經是全屏if (this.isFullScreen) {// 退出全屏if (document.exitFullscreen) {document.exitFullscreen();} else if (document.webkitCancelFullScreen) {document.webkitCancelFullScreen();} else if (document.mozCancelFullScreen) {document.mozCancelFullScreen();} else if (document.msExitFullscreen) {document.msExitFullscreen();}} else {// 全屏if (element.requestFullscreen) {element.requestFullscreen();} else if (element.webkitRequestFullScreen) {element.webkitRequestFullScreen();} else if (element.mozRequestFullScreen) {element.mozRequestFullScreen();} else if (element.msRequestFullscreen) {// IE11element.msRequestFullscreen();}}this.isFullScreen = !this.isFullScreen;},// 搜索search() {this.isShow = !this.isShow;},// 保存submit() {const data = this.graph.getNodes();this.$emit('submitEr', data);},// 檢索valChange(val) {if (val) {// false - 清空const nodes = this.graph.getNodes() || [];const node = nodes.filter(item => item.id === val)[0] || {};this.graph.centerCell(node); // 將節點/邊的中心與視口中心對齊} else {this.resetFn();}},},
};
</script><style lang="scss" scoped>
.antv-x6 {width: 100%;height: 100%;padding: 0;display: flex;position: relative;box-sizing: border-box;-moz-box-sizing: border-box;-webkit-box-sizing: border-box;::v-deep body {min-width: auto;}.node-c {width: 200px;border-right: 1px solid #eee;padding: 20px;dl {margin-bottom: 20px;line-height: 30px;display: flex;cursor: move;dt {&.circle {width: 30px;height: 30px;border-radius: 50%;&.start {border: 1px solid green;background: greenyellow;}&.end {border: 1px solid salmon;background: red;}}&.rect {width: 30px;height: 30px;border: 1px solid #ccc;}}dd {font-size: bold;font-size: 14px;padding: 0 0 0 10px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}}}.template-c {padding: 10px 0;li {line-height: 40px;font-size: 14px;border-bottom: 1px solid #dcdfe6;cursor: pointer;display: flex;justify-content: space-between;span {flex: 1;padding-right: 10px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}i {font-size: 14px;color: #2d8cf0;width: 20px;line-height: 40px;}}}.container {flex: 1;}.operating {position: absolute;z-index: 999;right: 20px;top: 10px;padding: 5px 10px;border-radius: 6px;background-color: #ffffff;border: 1px solid rgb(187, 187, 187);box-shadow: 1px 1px 4px 0 #0a0a0a2e;display: flex;height: 34px;align-items: center;.el-select {transition: width 0.6s ease-in-out;::v-deep .el-input__inner {height: 26px;line-height: 26px;}::v-deep .el-input--mini .el-input__icon {line-height: 26px;}::v-deep .el-select-dropdown__item {height: 48px;max-width: 410px;line-height: 48px;}&.hideSelect {width: 0px;::v-deep .el-input__inner {display: none;}::v-deep .el-input__suffix {display: none;}}&.showSelect {width: 180px;::v-deep .el-input__inner {display: block;}::v-deep .el-input__suffix {display: block;}}}.icon_oper {svg {font-size: 18px;cursor: pointer;margin: 0 5px;&:hover {color: #2d8cf0;}&.opacity {opacity: 0.5;}}}}
}
.app-mini {position: fixed;z-index: 999;bottom: 10px;right: 20px;border-radius: 6px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.app-content {flex: 1;height: 100% !important;
}
::v-deep .x6-graph-scroller {border: 1px solid #f0f0f0;margin-left: -1px;width: 100% !important;height: 100% !important;
}.head_top {width: 100%;height: 48px;display: flex;align-items: center;.code_warp {width: 90%;height: 100%;font-size: 12px;margin-left: 8px;display: flex;gap: 4px;flex-direction: column;justify-content: center;.code {color: black;font-weight: 700;line-height: normal;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;word-break: break-all;}.name {color: #b3b2bf;font-weight: 600;line-height: normal;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;word-break: break-all;}}
}
::v-deep .text {height: 32px;display: flex;gap: 1px;font-size: 13px;position: relative;padding-left: 20px;align-items: center;svg {position: absolute;left: 4px;top: 10px;}.type {width: 25%;height: 24px;font-size: 12px;line-height: 24px;text-align: center;border-radius: 4px;margin-right: 5px;display: inline-block;background-color: #f7f7f9;}span {flex: 1;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;word-break: break-all;line-height: 18px;}&:hover {background: #f8f8fa;}
}
</style>