一、 首先 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, // 网格大小 10px
visible: 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自定义节点
ref="node_dom"
class="node_warp"
:style="{
width: node.size.width + 'px',
height: node.size.height + 'px',
borderTopColor: color,
}"
>
{{ node.code }}
{{ node.name }}
ref="popoverDom" placement="bottom-end" width="60" :value="popShow" trigger="click" popper-class="filter_column_popover" @hide="popShow = false" @show="popShow = true" > slot="reference" class="icon" type="primary" size="mini" style="opacity: 0.5;" icon-class="table_column_settings" > v-model="checkAll" :indeterminate="isIndeterminate" @change="handleCheckAllChange" > 全选
v-model="checkList" @change="handleCheckedCitiesChange" > {{ item }}
v-for="(item, index) in node.columnVersions"
:key="index"
class="text "
>
{{ item.code }}
{{ item.dataType }}
{{ item.name }}
v-if="!node.columnVersions || !node.columnVersions.length"
class="empy flex"
>
暂无数据
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 = ['英文名称', '字段类型'];
},
},
};
.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;
}
}
六、注册引入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主页面 全部代码
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, // 网格大小 10px
visible: 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:1
resetFn() {
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) {
// IE11
element.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();
}
},
},
};
.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;
}
}
精彩内容
发表评论