前言
需求:集团公司风险链路图
首次接触@antv/g6,进行记录总结
点击框显示公司数据
实现步骤
安装"@antv/g6": "4.5.0",
"insert-css": "^2.0.0",
安装
npm install --save @antv/g6
学习方式:
官网
https://antv.vision/zh/
蚂蚁金服 G6 文档
https://www.bookstack.cn/books/antv-g6
<template>
<div id="mindMapContainer" />
</template>
<style scoped lang='scss'>
#mindMapContainer {
width: 100%;
height: 100%;
position: relative;
::v-deep canvas{
position: absolute;
left: 0;
top:0;
right:0;
top: 0;
}
}
.tooltipDirlog{
padding: 0;
}
::v-deep .g6-component-contextmenu{
z-index: 2022;
}
</style>
<script lang='ts' setup>
import { ElMessage } from 'element-plus';
import {
ref, onMounted, watch, defineEmits,
} from 'vue'
import insertCss from 'insert-css';
import G6, {
IG6GraphEvent, IGroup, INode, Item, ModelConfig, ShapeOptions, TreeGraph,
} from '@antv/g6';
onMounted(() => {
insertCss(`
.g6-component-tooltip {
background-color: rgba(0,0,0, 0.65);
padding: 10px;
box-shadow: rgb(174, 174, 174) 0px 0px 10px;
width: fit-content;
color: #fff;
border-radius = 4px;
}
`);
})
const devicetopologyChart = ref<any>()
const destroy = () => {
if (devicetopologyChart.value) {
devicetopologyChart.value.clear();
devicetopologyChart.value.destroy();
}
};
defineExpose({
initContainer,
destroy,
});
- initContainer初始化函数用于进入页面绘图调用
const initContainer = async (Data: any) => {
const colors = {
B: '#5B8FF9',
R: '#F46649',
L: '#F46649',
Y: '#EEBC20',
G: '#5BD8A6',
DI: '#A7A7A7',
};
const props = {
data: Data,
config: {
padding: [20, 50],
defaultLevel: 3,
defaultZoom: 0.8,
modes: { default: ['zoom-canvas', 'drag-canvas'] },
},
};
const container = document.getElementById('mindMapContainer') as HTMLElement;
const width = container.scrollWidth;
const height = container.scrollHeight || 500;
const registerFn = () => {
};
registerFn();
const { data } = props;
let graph:TreeGraph;
const initGraph = (data: any) => {
}
initGraph(data);
if (typeof window !== 'undefined') {
window.onresize = () => {
if (!graph || graph.get('destroyed')) return;
if (!container || !container.scrollWidth || !container.scrollHeight) return;
graph.changeSize(container.scrollWidth, container.scrollHeight);
};
}
}
使用g6的G6.registerNode('节点名称','节点配置')
绘制位置如下图所示
根据cfg中的label进行区别绘制,绘制矩形和文字以及进行颜色填充
设置name便于监听点击事件
const registerFn = () => {
G6.registerNode(
'flow-rect',
{
draw(cfg: any, group:IGroup) {
const {
title = '',
collapsed,
label,
meta,
} = cfg;
const rectConfig = {
width: label === 'Downstream' || label === 'Upstream' ? 124 : 164,
height: label === 'Downstream' || label === 'Upstream' ? 82 : 32,
lineWidth: 1,
fontSize: 14,
fill: '#fff',
radius: 4,
stroke: '#26A6F5',
opacity: 1,
};
const nodeOrigin = {
x: -rectConfig.width / 2,
y: -rectConfig.height / 2,
};
let rect;
if (label === 'Enterprise') {
rect = group.addShape('rect', {
attrs: {
x: nodeOrigin.x,
y: nodeOrigin.y,
fill: '#2878FF',
fontSize: 14,
lineWidth: 1,
width: 164,
height: 32,
radius: 4,
},
});
const base = group.addShape('text', {
attrs: {
x: 5,
y: 22 + nodeOrigin.y,
text: (title as string).length > 12 ? `${(title as string).substring(0, 12)}...` : title,
textAlign: 'center',
fill: '#fff',
fontSize: 14,
},
});
} else if (label === 'Downstream') {
rect = group.addShape('rect', {
attrs: {
x: nodeOrigin.x,
y: nodeOrigin.y,
width: 124,
height: 82,
lineWidth: 1,
fontSize: 14,
fill: '#fff',
radius: 4,
stroke: '#26A6F5',
opacity: 1,
},
});
group.addShape('rect', {
attrs: {
x: nodeOrigin.x,
y: nodeOrigin.y,
width: 124,
height: 28,
fill: '#26a6f51a',
radius: [4, 4, 0, 0],
opacity: 1,
},
});
group.addShape('text', {
attrs: {
textAlign: 'left',
textBaseline: 'bottom',
x: 12 + nodeOrigin.x,
y: 22 + nodeOrigin.y,
text: (title as string).length > 12 ? `${(title as string).substring(0, 12)}...` : title,
fontSize: 12,
fill: '#26A6F5',
cursor: 'pointer',
opacity: 1,
},
});
group.addShape('image', {
attrs: {
x: 12 + nodeOrigin.x,
y: 39 + nodeOrigin.y,
height: 10,
width: 10,
cursor: 'pointer',
opacity: 1,
img: 'https://huoxian-asr.oss-cn-beijing.aliyuncs.com/app/zichan.png',
},
name: 'node-icon',
});
group.addShape('text', {
attrs: {
textAlign: 'left',
textBaseline: 'bottom',
x: 28 + nodeOrigin.x,
y: 50 + nodeOrigin.y,
text: `资产:${mockData.value.children[0].meta.assets}`,
fontSize: 12,
opacity: 1,
fill: '#333333',
cursor: 'pointer',
},
});
group.addShape('image', {
attrs: {
x: 12 + nodeOrigin.x,
y: 59 + nodeOrigin.y,
height: 10,
width: 10,
cursor: 'pointer',
img: 'https://huoxian-asr.oss-cn-beijing.aliyuncs.com/app/fengxian.png',
},
name: 'node-icon',
});
group.addShape('text', {
attrs: {
textAlign: 'left',
textBaseline: 'bottom',
x: 28 + nodeOrigin.x,
y: 70 + nodeOrigin.y,
text: `风险:${mockData.value.children[0].meta.risk}`,
fontSize: 12,
opacity: 0.85,
fill: '#333333',
cursor: 'pointer',
},
});
}else {
rect = group.addShape('rect', {
attrs: {
x: nodeOrigin.x,
y: nodeOrigin.y,
...rectConfig,
},
});
group.addShape('text', {
attrs: {
textAlign: 'left',
textBaseline: 'bottom',
x: 12 + nodeOrigin.x,
y: 22 + nodeOrigin.y,
text: (title as string).length > 12 ? `${(title as string).substring(0, 12)}...` : title,
fontSize: 12,
opacity: 0.85,
fill: '#000',
cursor: 'pointer',
},
name: 'name-shape',
});
if (cfg.meta && cfg.meta.top) {
const top = group.addShape('text', {
attrs: {
x: 60,
y: -18,
fill: cfg.meta.top <= 5 ? '#ED7B2F' : '#fff',
text: cfg.meta.top <= 5 ? `TOP${cfg.meta.top}` : '',
fontSize: 12,
lineWidth: cfg.meta.top <= 5 ? 1 : 0,
},
});
const topBox = group.addShape('rect', {
attrs: {
x: 56,
y: -34,
fill: cfg.meta.top <= 5 ? '#ed7b2f1a' : '#fff',
fontSize: 12,
lineWidth: 1,
width: 40,
height: 18,
radius: 2,
},
});
}
if (cfg.meta && cfg.meta.status) {
const zhuangtai = group.addShape('text', {
attrs: {
x: cfg.meta.top <= 5 ? 22 : 54,
y: -18,
fill: '#00A870',
text: cfg.meta.status.slice(0, 2),
fontSize: 12,
lineWidth: 1,
},
});
const zhuangtaiBox = group.addShape('rect', {
attrs: {
x: cfg.meta.top <= 5 ? 14 : 44,
y: -34,
fill: '#00a8701a',
fontSize: 12,
lineWidth: 1,
width: 40,
height: 18,
radius: 2,
},
});
}
}
if (cfg.children && (cfg.children as Array<any>).length) {
group.addShape('rect', {
attrs: {
x: title === '上游' ? -rectConfig.width / 2 - 28 : rectConfig.width / 2 + 8,
y: -7,
width: 12,
height: 12,
radius: [6, 6],
stroke: 'rgba(0, 0, 0, 0.5)',
cursor: 'pointer',
fill: '#fff',
},
name: 'collapse-back',
modelId: cfg.id,
});
group.addShape('text', {
attrs: {
x: title === '上游' ? -rectConfig.width / 2 - 22 : rectConfig.width / 2 + 14,
y: -2,
textAlign: 'center',
textBaseline: 'middle',
text: collapsed ? '+' : '-',
fontSize: 12,
cursor: 'pointer',
fill: 'rgba(0, 0, 0, 0.7)',
},
name: 'collapse-text',
modelId: cfg.id,
});
}
this.drawLinkPoints(cfg, group);
return rect;
},
setState(name, value, item) {
if (name === 'collapse') {
const group = (item as any).getContainer();
const collapseText = group.find((e: any) => e.get('name') === 'collapse-text');
if (collapseText) {
if (!value) {
collapseText.attr({
text: '-',
});
} else {
collapseText.attr({
text: '+',
});
}
}
}
},
getAnchorPoints() {
return [
[0, 0.5],
[1, 0.5],
];
},
} as ShapeOptions,
'rect',
);
使用g6的G6.registerEdge('flow-edge','options')
参数:名称和数据项
绘制线段上的颜色、箭头以及线段上的提示
绘制的位置在下图所示
G6.registerEdge('flow-edge', {
afterDraw(cfg:any, group:IGroup | undefined) {
const shape = (group as IGroup).get('children')[0];
const midPoint = shape.getPoint(1);
const quatile = shape.getPoint(0.25);
cfg.targetNode._cfg.model.label === 'Downstream' && (group as IGroup).addShape('circle', {
attrs: {
r: 5,
fill: 'rgba(38, 166, 245, 1)',
x: quatile.x,
y: quatile.y,
},
});
cfg.targetNode._cfg.model.label === 'Upstream' && (group as IGroup).addShape('circle', {
attrs: {
r: 5,
fill: 'rgba(116, 115, 255, 1)',
x: quatile.x,
y: quatile.y,
},
});
cfg.targetNode._cfg.model.meta?.edge && cfg.targetNode._cfg.model.label !== 'chainSupply' && (group as IGroup).addShape('rect', {
attrs: {
width: 48,
height: 20,
fill: cfg.targetNode._cfg.model.meta.edge === '其他' ? 'rgba(240, 242, 245, 1)' : cfg.targetNode._cfg.model.meta.edge === '招标' && 'rgba(242, 241, 255, 1)' || 'rgba(233, 246, 254, 1)',
x: midPoint.x > 0 ? midPoint.x / 2 + midPoint.x / 4 + 10 : midPoint.x / 2 - midPoint.x / 4,
y: midPoint.y - 9,
},
});
cfg.targetNode._cfg.model.meta?.edge && cfg.targetNode._cfg.model.label !== 'chainSupply' && (group as IGroup).addShape('text', {
attrs: {
width: 10,
height: 10,
fill: cfg.targetNode._cfg.model.meta.edge === '其他' ? 'rgba(101, 119, 151, 1)' : cfg.targetNode._cfg.model.meta.edge === '招标' && 'rgba(116, 115, 255, 1)' || 'rgba(38, 166, 245, 1)',
x: midPoint.x > 0 ? midPoint.x / 2 + midPoint.x / 4 + 16 : midPoint.x / 2 - midPoint.x / 4 - 10,
y: midPoint.y + 10,
text: cfg.targetNode._cfg.model.meta.edge,
},
});
cfg.targetNode._cfg.model.label === 'chainSupply' && (group as IGroup).addShape('rect', {
attrs: {
width: 42,
height: 20,
fill: cfg.targetNode._cfg.model.meta.edge === '其他' ? 'rgba(240, 242, 245, 1)' : cfg.targetNode._cfg.model.meta.edge === '招标' && 'rgba(242, 241, 255, 1)' || cfg.targetNode._cfg.model.meta.edge === '投标' && 'rgba(230, 249, 249, 1)' || 'rgba(254, 249, 237, 1)',
x: cfg.targetNode._cfg.model.meta.form === 1 ? midPoint.x + 15 : midPoint.x - 66,
y: midPoint.y - 9,
},
});
update: undefined,
}, 'polyline');
const initGraph = (data: any) => {
if (!data) {
return;
}
const menu = new G6.Menu({
})
const tooltip = new G6.Tooltip({
})
graph = new G6.TreeGraph({
container: 'mindMapContainer',
width: container.scrollWidth,
height: container.scrollHeight,
modes: {
default: ['zoom-canvas', 'drag-canvas'],
},
renderer: 'svg',
fitView: false,
autoPaint: false,
animate: true,
defaultNode: {
type: 'flow-rect',
},
defaultEdge: {
type: 'flow-edge',
color: '#87e8de',
style: {
offset: 60,
lineAppendWidth: 80,
endArrow: true,
circle: 15,
stroke: '#D9D9D9',
},
},
layout: {
type: 'mindmap',
direction: 'H',
getHeight: () => 60,
getWidth: () => 50,
getVGap: () => 20,
getHGap: () => 120,
},
plugins: [tooltip, menu],
});
const handleCollapse = (e: IG6GraphEvent) => {
const target = e.target;
const id = target.get('modelId');
const item = graph.findById(id);
const nodeModel = item.getModel();
nodeModel.collapsed = !nodeModel.collapsed;
graph.layout();
graph.setItemState(item, 'collapse', nodeModel.collapsed as string);
};
graph.on('collapse-text:click', (e) => {
handleCollapse(e);
});
graph.on('collapse-back:click', (e) => {
handleCollapse(e);
});
graph.data(data);
graph.render();
graph.fitView(40);
devicetopologyChart.value = graph;
}
这一块是鼠标放上去调取接口,点击展示接口返回的信息
别问,问就是ts类型校验,内置自定义提示getContent函数不支持async
1、定义了三个参数
const companyDetail = ref() // 用于接收接口返回信息
const companyDetailShow = ref(false) // 鼠标移上去就调取接口,这个参数控制接口只执行一次
const companyDetailName = ref()
// 鼠标移上去就调取接口,这个参数控制接口只执行一次
提示函数用于调取公司信息
const tooltip = new G6.Tooltip({
offsetX: 20,
offsetY: 30,
className: 'tooltipDirlog',
itemTypes: ['node'],
fixToNode: [0.5, 0.5],
getContent: (e?: IG6GraphEvent | undefined) => {
const outDiv = document.createElement('div') as HTMLDivElement;
const nodeName = ((e as IG6GraphEvent).item as INode).getModel().title as string;
unicodeValue.value = ((e as IG6GraphEvent).item as INode).getModel().id
(async () => {
if (companyDetailName.value == nodeName && companyDetailShow.value) {
return false
}
const params = {
unicode: unicodeValue.value,
main_unicode: decode(queryUnicode.value),
}
const {
code,
msg,
data,
} = await (async (params: any) => {
return await requestSupplyMembers(params);
}
})(params);
companyDetail.value = data
})()
outDiv.innerHTML = `${formatedNodeName}`;
return '';
},
shouldBegin: (e?: IG6GraphEvent | undefined) => {
companyDetailName.value = ((e as IG6GraphEvent).item as INode).getModel().title;
companyDetailShow.value = true
if ((e as IG6GraphEvent).target.get('name') === 'name-shape' || (e as IG6GraphEvent).target.get('name') === 'mask-label-shape') {
companyDetailShow.value = true
return true;
}
return false;
},
});
通过上面我们调取接口获取到公司信息,点击展示弹出框显示公司信息
const menu = new G6.Menu({
offsetX: 6,
offsetY: 10,
itemTypes: ['node'],
trigger: 'click',
getContent: (e?: IG6GraphEvent | undefined) => {
const outDiv = document.createElement('div') as HTMLDivElement;
const nodeName = ((e as IG6GraphEvent).item as INode).getModel().title as string;
let formatedNodeName = '';
for (let i = 0; i < nodeName.length; i++) {
formatedNodeName = `${formatedNodeName}${nodeName[i]}`;
if (i !== 0 && i % 20 === 0) formatedNodeName = `${formatedNodeName}
`;
}
outDiv.innerHTML = `${formatedNodeName}`;
return `
${companyDetail
?.value
?.logo
=== '' ? '@/assets/images/logoDefault.png' : companyDetail
?.value
?.logo
}" style="width: 40px;height: 40px;" alt="">
${companyDetail?.value?.name}
风险数量:
${companyDetail?.value?.risk.software}
软件
${companyDetail?.value?.risk.app}
合规
${companyDetail?.value?.risk.email}
邮箱
${companyDetail?.value?.risk.port}
端口
${companyDetail?.value?.risk.code_leak}
代码
${companyDetail?.value?.risk.net_disk}
网盘
资产数量:
${companyDetail?.value?.assert.domain}
域名
${companyDetail?.value?.assert.subdomain}
子域名
${companyDetail?.value?.assert.ip}
IP
${companyDetail?.value?.assert.web}
网站
${companyDetail?.value?.assert.email}
泄露邮箱
${companyDetail?.value?.assert.certificate}
证书
${companyDetail?.value?.assert.app}
APP应用
${companyDetail?.value?.assert.cloud_product}
云产品
`;
},
shouldBegin: (e?: IG6GraphEvent | undefined) => {
companyDetailName.value = ((e as IG6GraphEvent).item as INode).getModel().title;
if (((e as IG6GraphEvent).item as INode).getModel().title === '上游') {
return false;
}
if ((e as IG6GraphEvent).target.get('name') === 'name-shape' || (e as IG6GraphEvent).target.get('name') === 'mask-label-shape' || ((e as IG6GraphEvent).item as INode).getModel().label === 'chainSupply') {
return true;
}
return false;
},
});
项目拓展
思路说明:当页面数据成千上万时,数据加载很慢,
所以后期做了一个树展示5条,通过点击更多加载下五条数据;
核心:updateChildren(data, parentId)
更新数据,差量更新子树中的所有子节点。data 是一个子树数据数组。
若希望更新或增加一个 parentId 节点的子节点,请使用 updateChild。
1、
新建一个对象,itemAddObj
后台数据遍历:每个对象item中mate中返回total字段判断是否需要更多显示,
total大于5时进行定义一个参数保存当前的父级id、下游list、当前页码、条数等
itemAddObj.value[`${item?.id}1`] = {
page: 1,
pageSize: 5,
list: item.children,
id: `${item?.id}1`,
total: item.meta?.total,
trend: item.title === '下游' ? -1 : 1,
root: item.meta.root,
}
2、当前的父级push一个自定义更多项
item.children.push({
children: null,
id: `${item.id}1`,
label: 'moreOver',
meta: {
edge: '', status: '存续(在营、开业、在册)', top: 180, trend: item.title === '上游' ? 1 : -1,
},
title: '更多',
})
3、自定义节点时 添加一个name:moreOver-text
group.addShape('rect', {
attrs: {
x: meta.trend === -1 ? nodeOrigin.x : nodeOrigin.x + 60,
y: nodeOrigin.y,
cursor: 'pointer',
width: 104,
height: 28,
fill: '#26a6f51a',
radius: [4, 4, 0, 0],
},
name: 'moreOver-text',
modelId: cfg.id,
});
4、监听它的点击事件
graph.on('moreOver-text:click', (e) => {
moreOver(e);
});
const moreOver = (e: IG6GraphEvent) => {
const target = e.target;
const id = target.get('modelId');
const item = graph.findById(id);
itemAddObj.value[id].page++;
emit('moveList', itemAddObj.value[id])
};
5、watch监听参数变化
watch(() => props.addListObj, () => {
const obj = itemAddObj.value[props.addListObj.id]
DateList.forEach((item:any) => {
obj.list.splice(obj.list.length - 1, 0, item)
});
const id = props.addListObj.id.slice(0, props.addListObj.id.length - 1)
if (obj.page * obj.pageSize > obj.total) {
obj.list = obj.list.slice(0, obj.list.length - 1)
}
devicetopologyChart.value.updateChildren(obj.list, id)
});