目录
简介
scss
快速上手 · 语雀
简介
antv/g6是一款基于JavaScript的图形可视化引擎,由阿里巴巴的AntV团队开发。
创建各种类型的图形,如流程图、关系图、树形图等。
G6采用了自己的绘图模型和渲染引擎,使其具备高性能的图形渲染能力。
它支持SVG和Canvas两种渲染方式,并且可以在Web和移动端应用中使用。
注册自定义节点、注册行为
<template>
<div class="custome-G6">
<div :id="containerId"></div>
<mds-modal class="custome-G6-modal" :visibility.sync="moreModal.visibility" title="选择操作" width="300px" :mask="true"
:footer="false" :showClose="true">
<div class="more-content">
<mds-button v-if="currentModel && currentModel.type !== 'node-root'" ghost type="primary"
@click="editNode">修改指标名称</mds-button>
<mds-button v-if="currentModel && currentModel.indexFlag === 1" ghost type="primary"
@click="addNode('sub')">添加下级指标</mds-button>
<mds-button v-if="currentModel && currentModel.indexFlag === 1" ghost type="primary"
@click="addNode('leaf')">添加底层指标</mds-button>
<mds-button v-if="currentModel && currentModel.type !== 'node-root'" ghost type="danger"
@click="handleDeleteNode">删除指标</mds-button>
</div>
</mds-modal>
<!-- 添加指标弹窗 -->
<mds-modal class="custome-G6-modal" :visibility.sync="addModal.visibility" :title="addModal.title" width="300px"
:mask="false" :showClose="true" okText="确定" @ok="handleAddNode" @close="handleClose">
<div style="height: 100px">
<template v-if="addModal.nodeType === 'leaf'">
<mds-select v-model="addModal.leaf" value-key="id" placeholder="请选择" filterable @change="changeLeaf">
<mds-option v-for="item in quaryScoreIndexList" :key="item.id" :value="item"
:label="item.indexNm"></mds-option>
</mds-select>
<div class="tip-text">请选择1个底层指标</div>
</template>
<template v-else>
<mds-input v-model="addModal.content.indexName" :maxlength="30"></mds-input>
<div class="tip-text">请填写下级指标名称,不超过30字</div>
</template>
</div>
</mds-modal>
<mds-modal class="custome-G6-modal" :visibility.sync="deleteModal.visibility" title="删除指标提示" width="300px"
:mask="false" :showClose="true" okText="确定" @ok="deleteNode" @close="closeDelete">
<div style="height: 100px">
<div>将删除 “<span style="font-weight:bold">{{ currentModel && currentModel.indexName }}</span>”
<template v-if="currentModel && currentModel.type === 'node-sub'">及其<span
style="font-weight:bold">所有下级指标</span></template>
,确定吗?
</div>
</div>
</mds-modal>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch, Emit } from 'vue-property-decorator'
import G6 from '@antv/g6'
@Component({
components: {}
})
export default class CustomeG6 extends Vue {
@Prop({ required: true }) private containerId!: string
@Prop({ required: true }) private indexContent!: any
@Prop({ required: true }) private quaryScoreIndexList!: any
@Prop({ required: true }) private disabled!: boolean
// 更新根节点名称
@Watch('indexContent.indexName', { deep: true })
changeIndexName(val: any, old: any) {
// 获取树的根节点
let rootNode = this.tree.getNodes()[0];
// 更新根节点的索引名为新的值
this.tree.updateItem(rootNode, { indexName: val });
// 渲染更新后的树
this.tree.render();
}
private tree: any
private moreModal: any = {
visibility: false
}
private addModal: any = {
visibility: false,
title: '添加下级指标',
leaf: '',
content: {
indexName: '',
indexCode: null
},
nodeType: 'leaf',
opType: 'add'
}
private deleteModal: any = {
visibility: false,
}
currentEvt: any = null
currentModel: any = null
currentAction = ''
// 关闭删除指标弹窗
closeDelete() {
this.deleteModal.visibility = false
}
// 打开删除指标弹窗
handleDeleteNode() {
this.deleteModal.visibility = true
}
// 确定删除指标
deleteNode() {
const parent = this.currentEvt.item.get('parent');
const model = this.currentEvt.item.get('model');
this.currentEvt.currentTarget.updateItem(parent, {
children: (parent.get('model').children || []).filter((e: any) => e.id !== model.id),
});
this.currentEvt.currentTarget.layout(false);
this.closeDelete()
this.moreModal.visibility = false
this.$emit('update:indexContent', this.tree.get('data'))
}
// 修改指标名称
editNode() {
const model = this.currentEvt.item.get('model');
if (this.currentModel.type === 'node-leaf') {
this.addModal.content.indexCode = model.indexCode
this.addModal.leaf = {
id: model.indexCode,
indexNm: model.indexName
}
} else {
this.addModal.content.indexCode = ''
}
this.addModal.nodeType = this.currentModel.type === 'node-leaf' ? 'leaf' : 'sub'
this.addModal.content.indexName = model.indexName
this.addModal.opType = 'edit'
this.addModal.title = '修改指标名称'
this.addModal.visibility = true
}
// 关闭添加指标弹窗
handleClose() {
this.addModal.content.indexName = ''
this.addModal.content.indexCode = ''
this.addModal.visibility = false
console.log('关闭添加指标弹窗')
}
addNode(type: string) {
this.addModal.opType = 'add'
this.addModal.nodeType = type
this.addModal.title = `添加${type === 'leaf' ? '底层' : '下级'}指标`
this.addModal.visibility = true
}
// 添加指标
handleAddNode() {
if (!this.addModal.content.indexName.trim()) {
this.$message.error(this.addModal.nodeType === 'sub' ? '请输入下级指标' : '请选择底层指标')
return
}
if (this.addModal.nodeType === 'sub') {
this.addModal.content.indexCode = ''
}
const model = this.currentEvt.item.get('model');
// console.log('点击的name::::', name)
const newId = model.id + '-' +
(((model.children || []).reduce((a: any, b: any) => {
const num = Number(b.id.split('-').pop());
return a < num ? num : a;
}, 0) || 0) +
1);
let obj
if (this.addModal.opType === 'add') {
obj = {
children: (model.children || []).concat([{
id: newId,
direction: 'right',
indexFlag: this.addModal.nodeType === 'sub' ? 1 : 2,
indexCode: this.addModal.content.indexCode,
indexName: this.addModal.content.indexName,
children: [],
type: this.addModal.nodeType === 'sub' ? 'node-sub' : 'node-leaf',
color: '#aaa',
},]),
}
console.log('添加指标:', this.addModal.nodeType, obj)
} else {
obj = {
indexName: this.addModal.content.indexName,
indexCode: this.addModal.content.indexCode
}
}
this.currentEvt.currentTarget.updateItem(this.currentEvt.item, obj);
this.currentEvt.currentTarget.layout(false);
this.addModal.visibility = false
this.addModal.content.indexName = ''
this.addModal.content.indexCode = null
this.addModal.leaf = ''
this.moreModal.visibility = false
this.$emit('update:indexContent', this.tree.get('data'))
}
// 选择底层指标
changeLeaf(val: any) {
if (!val) {
this.addModal.content.indexName = ''
this.addModal.content.indexCode = ''
return
}
this.addModal.content.indexName = val.indexNm
this.addModal.content.indexCode = val.id
}
updateTree() {
this.tree.data(this.indexContent)
this.tree.render()
}
mounted() {
const _this = this
const { Util } = G6;
// <text style={{ marginLeft: ${width - 16}, marginTop: -18, stroke: '', fill: '#000', fontSize: 16, cursor: 'pointer', opacity: ${cfg.hover ? 0.75 : 0} }} action="add">+</text>
// <group zIndex=9999>
// <rect style={{ width: 100, height: 42, stroke: ${stroke}, fill: ${fill}, marginLeft: ${ width + 30 }, marginTop: -24, cursor: 'pointer', opacity: ${cfg.openMore ? 1 : 0} }} action="addSub">
// <Text style={{ marginLeft: ${ width + 42 }, marginTop: 12, cursor: 'pointer', opacity: ${cfg.openMore ? 1 : 0} }} action="addSub">添加下级指标</Text>
// </rect>
// <rect style={{ width: 100, height: 42, stroke: ${stroke}, fill: ${fill}, marginLeft: ${ width + 30 }, marginTop: -24, cursor: 'pointer', opacity: ${cfg.openMore ? 1 : 0} }} action="addLeaf">
// <Text style={{ marginLeft: ${ width + 42 }, marginTop: 12, cursor: 'pointer', opacity: ${cfg.openMore ? 1 : 0} }} action="addLeaf">添加底层指标</Text>
// </rect>
// </group>
// 根结点
// 使用 G6.registerNode() 方法注册一个名为 'node-root' 的自定义节点
G6.registerNode(
'node-root', // 节点名称,这里为 'node-root'
{
// jsx 属性指定节点的渲染函数,用于生成节点的 HTML/SVG 内容
jsx: (cfg: any) => {
// 计算节点内容的宽度,以便在渲染时使用
// 16: 文本字体大小 (font size)
// 它表示文本的最大宽度。在这里传递 [0] 作为参数,可能意味着测量文本的实际宽度,而不限制其最大宽度
// 24: 这是在计算节点内容宽度时额外添加的宽度值。在代码中,它被用作一个修正项,可能是为了确保节点的宽度足够容纳文本内容,并且在节点左右两侧留有一定的间隔
const width = Util.getTextSize(cfg.indexName, 16)[0] + 24;
// 获取节点样式中的边框颜色,默认为 '#CED4E0'
const stroke = cfg.style.stroke || '#CED4E0';
// 获取节点样式中的填充颜色,默认为 '#FFF'
const fill = cfg.style.fill || '#FFF';
// 返回节点的 HTML/SVG 内容
return `
<group>
<rect draggable="true" style={{width: ${width}, height: 42, stroke: ${stroke}, fill: ${fill}, radius: 8 }} keyshape>
<text style={{ fontSize: 16, marginLeft: 12, marginTop: 12 }}>${cfg.indexName}</text>
<Circle style={{ r: 10, fill: '#FFF', stroke: ${stroke}, marginLeft: ${width + 14}, marginTop: 4 }}>
<Text style={{ fill: ${_this.disabled ? '#ddd' : '#1564FF'}, fontSize: 18, lineHeight: 24, marginLeft: ${width + 7}, marginTop: -12, cursor: ${_this.disabled ? 'not-allowed' : 'pointer'} }} action="more">...</Text>
</Circle>
</rect>
</group>
`;
},
// getAnchorPoints() 方法定义节点的锚点位置,即连接边的起始和结束点
getAnchorPoints() {
// 返回一个数组,数组中包含两个锚点位置
// 第一个锚点位于节点的左边中点 [0, 0.5]
// 第二个锚点位于节点的右边中点 [1, 0.5]
return [
[0, 0.5],
[1, 0.5],
];
},
},
'single-node' // 节点类型,这里为 'single-node'
);
// 子节点
// <text style={{ marginLeft: ${width - 32}, marginTop: -18, fill: '#000', fontSize: 16, cursor: 'pointer', opacity: ${cfg.hover ? 0.75 : 0} }} action="add">+</text>
// <text style={{ marginLeft: ${width - 16}, marginTop: -34, fill: '#000', fontSize: 16, cursor: 'pointer', opacity: ${cfg.hover ? 0.75 : 0}, next: 'inline' }} action="delete">-</text>
G6.registerNode(
'node-sub', {
jsx: (cfg: any) => {
const width = Util.getTextSize(cfg.indexName, 14)[0] + 24;
const stroke = cfg.style.stroke || '#CED4E0';
const fill = cfg.style.fill || '#FFF';
const color = '#f00';
return `
<group>
<rect draggable="true" style={{width: ${width}, height: 42, stroke: ${stroke}, fill: ${fill}, radius: 8 }} keyshape>
<text style={{ fontSize: 14, marginLeft: 12, marginTop: 12 }}>${cfg.indexName}</text>
<Circle style={{ r: 10, fill: '#FFF', stroke: ${stroke}, marginLeft: ${width + 14}, marginTop: 4 }}>
<Text style={{ fill: ${_this.disabled ? '#ddd' : '#1564FF'}, fontSize: 18, marginLeft: ${width + 7}, marginTop: -12, cursor: ${_this.disabled ? 'not-allowed' : 'pointer'}, }} action="more">...</Text>
</Circle>
</rect>
</group>
`;
},
getAnchorPoints() {
return [
[0, 0.5],
[1, 0.5],
];
},
},
'single-node',
);
// 叶子节点
// <text style={{ marginLeft: ${width - 16}, marginTop: -18, stroke: ${color}, fill: '#000', cursor: 'pointer', opacity: ${cfg.hover ? 0.75 : 0}, next: 'inline' }} action="delete">-</text>
G6.registerNode(
'node-leaf', {
jsx: (cfg: any) => {
const width = Util.getTextSize(cfg.indexName, 14)[0] + 24;
const stroke = cfg.style.stroke || '#CED4E0';
const fill = cfg.style.fill || '#FFF';
const color = cfg.color || cfg.style.stroke;
return `
<group>
<rect draggable="true" style={{width: ${width}, height: 42, stroke: ${stroke}, fill: ${fill}, radius: 8}} keyshape>
<text style={{ fontSize: 14, marginLeft: 12, marginTop: 12 }}>${cfg.indexName}</text>
<Circle style={{ r: 10, fill: '#FFF', stroke: ${stroke}, marginLeft: ${width + 14}, marginTop: 4 }}>
<Text style={{ fill: ${_this.disabled ? '#ddd' : '#1564FF'}, fontSize: 18, marginLeft: ${width + 7}, marginTop: -12, cursor: ${_this.disabled ? 'not-allowed' : 'pointer'}, }} action="more">...</Text>
</Circle>
</rect>
</group>
`;
},
getAnchorPoints() {
return [
[0, 0.5],
[1, 0.5],
];
},
},
'single-node',
);
// 双击修改节点名称
editNode(evt: any) {
const item = evt.item;
const model = item.get('model');
// 根结点不能修改名称
if (model.type === 'node-root') return;
console.log('model:::---:', model);
// 获取节点位置
const { x, y } = item.calculateBBox();
// 获取图表对象
const graph = evt.currentTarget;
// 将节点位置转换为实际位置
const realPosition = evt.currentTarget.getClientByPoint(x, y);
// 创建一个文本输入框
const el = document.createElement('div');
const fontSizeMap: any = {
'node-root': 24,
'node-sub': 18,
'node-leaf': 18,
};
el.style.fontSize = fontSizeMap[model.type] + 'px';
el.style.position = 'fixed';
el.style.top = realPosition.y + 4 + 'px';
el.style.left = realPosition.x + 'px';
el.style.paddingLeft = '6px';
el.style.transformOrigin = 'top left';
el.style.transform = `scale(${evt.currentTarget.getZoom()})`;
const input = document.createElement('input');
input.style.border = 'none';
input.value = model.indexName;
input.style.width = Util.getTextSize(model.indexName, fontSizeMap[model.type])[0] + 'px';
input.className = 'dice-input';
el.className = 'dice-input';
el.appendChild(input);
document.body.appendChild(el);
// 定义销毁文本输入框的函数
const destroyEl = () => {
document.body.removeChild(el);
};
// 定义处理点击事件的函数
const clickEvt = (event: any) => {
if (!(event.target && event.target.className && event.target.className.includes('dice-input'))) {
// 移除事件监听器
window.removeEventListener('mousedown', clickEvt);
window.removeEventListener('scroll', clickEvt);
// 更新节点名称并重新布局图表
graph.updateItem(item, {
indexName: input.value,
});
graph.layout(false);
// 移除滚轮缩放事件监听器,并销毁文本输入框
graph.off('wheelZoom', clickEvt);
destroyEl();
}
};
// 添加事件监听器,处理点击事件
graph.on('wheelZoom', clickEvt);
window.addEventListener('mousedown', clickEvt);
window.addEventListener('scroll', clickEvt);
// 监听输入框的键盘事件,如果按下 Enter 键,触发点击事件
input.addEventListener('keyup', (event) => {
if (event.key === 'Enter') {
clickEvt({
target: {},
});
}
});
},
hoverNode(evt: any) {
evt.currentTarget.updateItem(evt.item, {
hover: true,
});
},
hoverNodeOut(evt: any) {
evt.currentTarget.updateItem(evt.item, {
hover: false,
});
},
});
G6 图形库的行为(Behavior),用于在画布上实现缩放和平移操作。当用户在画布上滚动鼠标滚轮时,会根据情况执行缩放或平移操作。如果同时按下了 Ctrl 键,则进行缩放操作,否则进行平移操作。
// 在 G6 中注册名为 'scroll-canvas' 的行为
G6.registerBehavior('scroll-canvas', {
// 获取事件列表
getEvents: function getEvents() {
return {
wheel: 'onWheel', // 当滚轮滚动事件发生时,调用 onWheel 方法
};
},
// 处理滚轮滚动事件的方法
onWheel: function onWheel(ev: any) {
const { graph } = _this; // 从 this 对象中获取 graph,这里的 _this 表示当前行为实例
if (!graph) {
return;
}
if (ev.ctrlKey) { // 如果按下了 Ctrl 键
const canvas = graph.get('canvas'); // 获取画布对象
const point = canvas.getPointByClient(ev.clientX, ev.clientY); // 根据鼠标位置获取画布上的坐标点
let ratio = graph.getZoom(); // 获取当前图形的缩放比例
if (ev.wheelDelta > 0) { // 如果滚轮向上滚动
ratio += ratio * 0.05; // 将缩放比例增加 5%
} else {
ratio *= ratio * 0.05; // 否则将缩放比例减少 5%
}
graph.zoomTo(ratio, {
x: point.x, // 设置缩放中心点的 x 坐标
y: point.y, // 设置缩放中心点的 y 坐标
});
} else {
const x = ev.deltaX || ev.movementX; // 获取水平方向上的滚动距离
const y = ev.deltaY || ev.movementY || (-ev.wheelDelta * 125) / 3; // 获取垂直方向上的滚动距离
graph.translate(-x, -y); // 平移图形,向相反方向移动
}
ev.preventDefault(); // 阻止默认滚动事件,避免影响整个页面的滚动
},
});
- 节点被点击时,触发'node:click'事件,调用'clickNode'函数。
- 节点被双击时,触发'node:dblclick'事件,原本预计调用'editNode'函数,但该函数体被注释掉了。
- 鼠标进入节点时,触发'node:mouseenter'事件,调用'hoverNode'函数。
- 鼠标离开节点时,触发'node:mouseleave'事件,调用'hoverNodeOut'函数。
// 假设这是一个名为G6的图形引擎,通过registerBehavior注册了一个名为'dice-mindmap'的行为
G6.registerBehavior('dice-mindmap', {
// 获取事件列表的方法
getEvents() {
return {
// 当节点被点击时触发'node:click'事件,调用'clickNode'方法
'node:click': 'clickNode',
// 当节点被双击时触发'node:dblclick'事件,但该行为被注释掉了,没有调用对应的方法
// 'node:dblclick': 'editNode',
// 当鼠标进入节点时触发'node:mouseenter'事件,调用'hoverNode'方法
'node:mouseenter': 'hoverNode',
// 当鼠标离开节点时触发'node:mouseleave'事件,调用'hoverNodeOut'方法
'node:mouseleave': 'hoverNodeOut',
};
},
// 节点被点击时调用的方法
clickNode(evt: any) {
// 获取节点相关信息
const model = evt.item.get('model');
const name = evt.target.get('action');
_this.currentAction = name; // 假设_this是之前定义过的变量,用于保存当前的动作名称
switch (name) {
// case 'addSub':
// case 'addLeaf':
// // 添加子节点或叶节点的逻辑代码
// // ...
// break;
// case 'delete':
// // 删除节点的逻辑代码
// // ...
// break;
// case 'edit':
// // 编辑节点的逻辑代码
// console.log('edit::::')
// break;
case 'more':
// 如果当前没有被禁用
if (!_this.disabled) {
// 假设_this是之前定义过的变量,用于保存当前的事件和节点模型
_this.currentEvt = evt;
_this.currentModel = model;
// 打印当前节点模型信息
console.log('currentModel::::', _this.currentModel);
// 假设_moreModal是之前定义过的变量,用于显示更多操作的弹窗
_this.moreModal.visibility = true;
// 可以根据需要执行其他操作
// ...
}
break;
default:
// 如果没有匹配到任何动作名称,直接返回
return;
}
// 可以在这里添加其他代码逻辑
// ...
},
// 其他方法
// ...
});
将输入的数据对象进行转换,并根据不同层级进行相应的属性设置。在转换过程中,会对节点的类型、悬停状态、展开状态等进行处理,同时为部分节点设置默认值。如果节点包含子节点,会递归地处理子节点的数据。
// 定义数据转换函数 dataTransform,接收一个参数 data,该参数为任意类型的数据
const dataTransform = (data: any) => {
// 定义内部递归函数 changeData,接收两个参数:d 表示当前数据节点,level 表示当前数据节点的层级,默认值为 0
const changeData: any = (d: any, level = 0) => {
// 创建一个新的数据对象 data,用扩展运算符复制当前数据节点 d 的所有属性到新对象中
const data = {
...d,
};
// 使用 switch 语句根据当前节点层级 level 进行不同的处理
switch (level) {
case 0:
// 当层级为 0 时,设置节点的 type 属性为 'node-root'
data.type = 'node-root';
break;
// case 1:
// data.type = 'node-sub';
// break;
default:
// 默认情况下,设置节点的 type 属性为 'node-sub'
data.type = 'node-sub';
break;
}
// 设置节点的 hover 属性为 false,表示鼠标未悬停在节点上
data.hover = false;
// 设置节点的 openMore 属性为 false,表示未展开更多选项
data.openMore = false;
// 当节点层级为 1 且没有 direction 属性时,进行下面的处理
if (level === 1 && !d.direction) {
// 如果节点没有 direction 属性,则设置 direction 属性为 'right'
data.direction = 'right';
}
// 如果当前节点存在子节点,则递归处理每个子节点,并将返回的新数据添加到当前节点的 children 属性中
if (d.children) {
data.children = d.children.map((child: any) => changeData(child, level + 1));
}
// 返回处理后的新数据对象
return data;
};
// 调用递归函数 changeData,并传入初始的 data 参数进行数据转换
return changeData(data);
};
const container: any = document.getElementById(_this.containerId);
// const el = document.createElement('pre');
// el.innerHTML = '双击修改节点标题';
// container.appendChild(el);
const width = container.scrollWidth;
// const height = (container.scrollHeight || 500) - 20;
this.tree = new G6.TreeGraph({
container: _this.containerId,
width: width,
height: 300,
fitView: true,
fitViewPadding: [10, 20],
layout: {
type: 'mindmap',
direction: 'H',
nodesep: 80, // 可选
ranksep: 40, // 可选
// 节点高度
getHeight: () => {
return 16;
},
// 节点宽度
getWidth: (node: any) => {
return node.level === 0 ?
Util.getTextSize(node.indexName, 16)[0] + 12 :
Util.getTextSize(node.indexName, 12)[0];
},
// 节点之间的垂直间距
getVGap: () => {
return 40;
},
// 节点之间的水平间距
getHGap: () => {
return 84;
},
getSide: (node: any) => {
return node.data.direction;
},
},
defaultEdge: {
type: 'cubic-horizontal',
style: {
lineWidth: 2,
},
},
minZoom: 0.8,
maxZoom: 1.5,
modes: {
default: ['drag-canvas', 'zoom-canvas', 'dice-mindmap'],
},
});
const data = dataTransform(_this.indexContent)
this.$emit('update:indexContent', data)
this.tree.data(data);
this.tree.render();
if (typeof window !== 'undefined') {
window.onresize = () => {
if (!this.tree || this.tree.get('destroyed')) return;
if (!container || !container.scrollWidth || !container.scrollHeight) return;
this.tree.changeSize(container.scrollWidth, 300);
};
}
scss
<style lang="scss">
.custome-G6-modal {
.mds-modal {
min-width: initial;
}
.more-content {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
column-gap: 12px;
row-gap: 12px;
padding-bottom: 48px;
.mds-btn {
width: auto;
margin: 0;
}
}
.mds-modal-header,
.mds-modal-bottom {
border: none;
}
.mds-modal-footer-default {
justify-content: flex-end;
button {
flex: initial;
width: 80px;
}
.mds-modal-button {
margin-right: 2px;
}
}
.tip-text {
font-size: 12px;
line-height: 18px;
color: rgba(168, 172, 179, 1);
margin-top: 10px;
}
}
</style>
<style lang="scss" scoped>
.custome-G6 {
background-color: #F9F9F9;
}
</style>