项目介绍:
Vue3 + ts
antv/g6官网地址:g6.antv.antgroup.com/examples
实现效果:
首先:
pnpm install --save @antv/g6
//样式我这里额外使用了 inset-css,大家看情况参考
pnpm install --save @insert-css
直接上代码:
vue组件
<script setup lang="ts">
import G6, { TreeGraph } from '@antv/g6'
import insertCss from 'insert-css'
import useRegister from './useRegister'
import { transTree, iconMap } from './data'
import Drawer from './Drawer.vue'
const props = defineProps({
node: {
type: Object,
default: () => ({})
},
name: {
type: String,
default: ''
},
envId: {
type: Number,
default: 0
}
})
// 工具栏样式
insertCss(`
.g6-toolbar-ul {
width: 42px;
height: 96px;
position: absolute;
bottom: 20px;
left: 20px;
background: #ffffff;
padding-top: 15px;
padding-bottom: 15px;
box-shadow: 0px 1px 3px -2px rgba(7, 88, 202, 0.14),
0px 3px 8px 0px rgba(7, 88, 202, 0.1),
0px 5px 16px 4px rgba(7, 88, 202, 0.06);
border-radius: 21px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.g6-toolbar-ul .first::after {
content: '';
display: block;
width: 24px;
height: 1px;
background: #e8e8e8;
padding-left: 2px;
}
.g6-toolbar-ul .second {
margin-top: 10px;
}
`)
// tooltip 样式
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;
}
`)
// 画布实例和挂载容器
let graph: TreeGraph
let container: HTMLElement | null
const first = ref<boolean>(true)
// 基础配置
const visible = ref(false)
const currentNode = ref<{ [key: string]: any }>({
type: '',
label: '',
tooltip: ''
})
const url =
'ws://' +
import.meta.env.VITE_MANAGE_API.replace('http://', '') +
'service/ws/topology'
onMounted(() => {
container = document.getElementById('container')
getData()
})
// 获取数据
const getData = () => {
let nodeData = []
const { status, data, send, open, close } = useWebSocket(url, {
autoReconnect: {
retries: 3,
delay: 1000
// onFailed() {
// alert('Failed to connect topology after 3 retries')
// }
},
onMessage: (ws, e) => {
nodeData = transTree(JSON.parse(e.data))
G6.Util.traverseTree(nodeData[0], subtree => {
if (subtree.level === undefined) subtree.level = 0
subtree.children?.forEach(child => (child.level = subtree.level + 1))
switch (subtree.level) {
case 0:
subtree.type = 'root'
break
case 1:
subtree.type = 'treeNode'
break
default:
subtree.type = 'treeNode'
}
})
if (first.value) {
// 生成画布
createGraph(nodeData[0])
first.value = false
} else {
// 更新画布
graph.changeData(nodeData[0])
}
}
})
send(JSON.stringify({ envId: Number(props.envId), serviceName: props.name }))
}
const toolbar = new G6.ToolBar({
className: 'g6-toolbar-ul',
getContent: () => {
return `
<ul>
<li class="li-style first" code='zoomOut'>
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M658.432 428.736a33.216 33.216 0 0 1-33.152 33.152H525.824v99.456a33.216 33.216 0 0 1-66.304 0V461.888H360.064a33.152 33.152 0 0 1 0-66.304H459.52V296.128a33.152 33.152 0 0 1 66.304 0V395.52H625.28c18.24 0 33.152 14.848 33.152 33.152z m299.776 521.792a43.328 43.328 0 0 1-60.864-6.912l-189.248-220.992a362.368 362.368 0 0 1-215.36 70.848 364.8 364.8 0 1 1 364.8-364.736 363.072 363.072 0 0 1-86.912 235.968l192.384 224.64a43.392 43.392 0 0 1-4.8 61.184z m-465.536-223.36a298.816 298.816 0 0 0 298.432-298.432 298.816 298.816 0 0 0-298.432-298.432A298.816 298.816 0 0 0 194.24 428.8a298.816 298.816 0 0 0 298.432 298.432z"></path>
</svg>
</li>
<li class="li-style second" code='zoomIn'>
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path d="M639.936 416a32 32 0 0 1-32 32h-256a32 32 0 0 1 0-64h256a32 32 0 0 1 32 32z m289.28 503.552a41.792 41.792 0 0 1-58.752-6.656l-182.656-213.248A349.76 349.76 0 0 1 480 768 352 352 0 1 1 832 416a350.4 350.4 0 0 1-83.84 227.712l185.664 216.768a41.856 41.856 0 0 1-4.608 59.072zM479.936 704c158.784 0 288-129.216 288-288S638.72 128 479.936 128a288.32 288.32 0 0 0-288 288c0 158.784 129.216 288 288 288z" p-id="3853"></path>
</svg>
</li>
</ul>
`
},
handleClick: (code, graph) => {
if (code === 'zoomOut') {
toolbar.zoomOut()
} else if (code === 'zoomIn') {
toolbar.zoomIn()
}
}
})
const tooltip = new G6.Tooltip({
offsetX: 20,
offsetY: 30,
// 允许出现 tooltip 的 item 类型
itemTypes: ['node'],
// 自定义 tooltip 内容
getContent: e => {
const outDiv = document.createElement('div')
const nodeName = e?.item?.getModel().name
const tag = e?.item?.getModel().namespace
outDiv.innerHTML = `${nodeName}<br/>${tag || ''} `
return outDiv
},
shouldBegin: e => {
if (e?.target.get('name') === 'label-shape') return true
return false
}
})
const createGraph = data => {
useRegister(G6)
const width = container?.scrollWidth || 1000
const height = container?.scrollHeight || 500
graph = new G6.TreeGraph({
container: 'container',
width,
height,
fitView: true,
enabledStack: true,
plugins: [toolbar, tooltip],
modes: {
// default: ['zoom-canvas', 'drag-canvas']
default: ['drag-canvas']
},
defaultNode: {
// type: 'treeNode',
anchorPoints: [
[0, 0.5],
[1, 0.5]
]
},
defaultEdge: {
type: 'cubic-horizontal',
style: {
radius: 10,
offset: 30
}
},
layout: {
type: 'compactBox',
direction: 'LR',
// 节点ID
getId: function getId(d: { uid: any }) {
return d.uid
},
// 节点高度
getHeight: function getHeight() {
return 60
},
// 节点宽度
getWidth: function getWidth() {
return 200
},
// // 节点垂直间隙
getVGap: function getVGap() {
return 15
},
// 节点水平间隙
getHGap: function getHGap() {
return 30
}
}
})
//先清除图表,再重新渲染
graph.clear()
graph.data(data)
graph.render()
graph.fitView()
//监听collapse点击
graph.on('collapse-text:click', e => {
handleCollapse(e)
})
graph.on('collapse-back:click', e => {
handleCollapse(e)
})
//点击展开弹窗
graph.on('node:click', e => {
const item = e.item // 被操作的节点 item
const target = e.target // 被操作的具体图形
if (
target.get('name') === 'collapse-back' ||
target.get('name') === 'collapse-text'
)
return
const model = item?.getModel()
//@ts-ignore
currentNode.value = model || null
if (currentNode.value.type !== 'root') visible.value = true
})
}
const handleCollapse = e => {
const target = e.target
const uid = target.get('modelId')
const item = graph.findById(uid)
const nodeModel = item.getModel()
nodeModel.collapsed = !nodeModel.collapsed
graph.layout()
//@ts-ignore
graph.setItemState(item, 'collapse', nodeModel.collapsed)
}
// 适配屏幕宽度
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)
}
}
provide('currentNode', currentNode)
provide('serviceName', props.name)
provide('envId', props.envId)
</script>
<template>
<div class="diagram">
<div id="container" ref="container"></div>
</div>
<el-drawer
v-model="visible"
direction="rtl"
size="60%"
destroy-on-close
append-to-body
>
//弹窗组件
<Drawer />
</el-drawer>
</template>
<style lang="scss" scoped>
.diagram {
position: relative;
width: 100%;
height: 550px;
margin-top: 10px;
background-color: #f7faff;
border-radius: 8px;
}
.headerImg {
display: inline-block;
vertical-align: bottom;
margin-left: -8px;
margin-right: 8px;
}
.headerText {
font-size: 18px;
font-weight: 600;
color: #323640;
}
.tag {
display: inline-block;
padding: 4px;
margin-right: 4px;
background: #f7faff;
border-radius: 2px;
border: 1px solid #b3d8ff;
color: #4290ff;
}
</style>
自定义钩子
主要用来定义节点样式:
import root from '@/assets/tuopu/root.png'
import heartG from '@/assets/tuopu/heart.png'
import heartR from '@/assets/tuopu/heartR.png'
import process from '@/assets/tuopu/process.png'
import leftArrow from '../img/leftArrow.png'
import rightArrow from '../img/rightArrow.png'
//一些图片的导入 可忽略
import { iconMap, kindMap } from './data'
export default G6 => {
//treeNode类型节点
G6.registerNode('treeNode', {
// 绘制节点,包含文本
/*
* @param {Object} cfg 节点的配置项 节点或边的配置项
* @param {G.Group} group 图形分组,节点中图形对象的容器
* @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 node.get('keyShape') 可以获取。
*/
draw: (cfg, group) => {
const size = [200, 56]
const keyShape = group.addShape('rect', {
attrs: {
width: size[0],
height: size[1],
stroke: '#D7DAE2',
lineWidth: 0.5,
x: -size[0] / 2,
y: -size[1] / 2,
fill: '#fff',
radius: 4
},
draggable: true,
name: 'root-keyshape'
})
group.addShape('rect', {
attrs: {
width: 35,
height: size[1],
x: -size[0] / 2,
y: -size[1] / 2,
fill: '#EFF5FE',
radius: 4
},
draggable: true,
name: 'root-keyshape'
})
group.addShape('rect', {
attrs: {
width: 10,
height: size[1],
x: -size[0] / 2 + 25,
y: -size[1] / 2,
fill: '#EFF5FE'
},
draggable: true,
name: 'root-keyshape'
})
group.addShape('image', {
attrs: {
x: -size[0] / 2 + 8,
y: -10,
width: 20,
height: 20,
img: iconMap.get(cfg.kind)
},
name: 'image-shape'
})
group.addShape('text', {
attrs: {
text:
cfg.name.length > 10 ? cfg.name.substr(0, 10) + '...' : cfg.name,
fill: '#000',
fontSize: 10,
x: -size[0] / 2 + 45,
y: -5
},
cursor: 'pointer',
name: 'label-shape'
})
group.addShape('image', {
attrs: {
x: -size[0] / 2 + 45,
y: 0,
width: 18,
height: 18,
img:
cfg.health === 'Processing'
? process
: cfg.health === 'Healthy'
? heartG
: cfg.health === 'Error'
? heartR
: ''
},
name: 'image1-shape'
})
group.addShape('text', {
attrs: {
text: kindMap.get(cfg.kind),
fill: '#4290FF',
fontSize: 10,
x: cfg.health ? -35 : -55,
y: 16
},
cursor: 'pointer',
name: 'text-shape'
})
// day tag
group.addShape('rect', {
attrs: {
x:
cfg.ago.length < 8
? size[0] / 2 - 50
: cfg.ago.length < 13
? size[0] / 2 - 75
: size[0] / 2 - 85,
y: -size[1] / 2 - 10,
width: cfg.ago.length < 9 ? 50 : cfg.ago.length < 13 ? 68 : 80,
height: 16,
stroke: '#D7DAE2',
lineWidth: 0.5,
fill: '#F7FAFF',
radius: 2
},
name: 'rect-tag'
})
group.addShape('text', {
attrs: {
text: cfg.ago,
fill: '#9098A9',
fontSize: 10,
x:
cfg.ago.length < 8
? size[0] / 2 - 45
: cfg.ago.length < 13
? size[0] / 2 - 68
: size[0] / 2 - 82,
y: -size[1] / 2 + 4
},
cursor: 'pointer',
name: 'text-tag'
})
// collapse circle
if (cfg.children && cfg.children.length) {
group.addShape('circle', {
attrs: {
x: size[0] / 2,
y: 0,
stroke: '#D7DAE2',
lineWidth: 0.5,
r: 8,
cursor: 'pointer',
fill: '#fff'
},
name: 'collapse-back',
modelId: cfg.uid
})
// collpase text
group.addShape('image', {
attrs: {
x: size[0] / 2 - 6,
y: -6,
width: 12,
height: 12,
img: cfg.collapsed ? rightArrow : leftArrow,
cursor: 'pointer'
},
name: 'collapse-text',
modelId: cfg.uid
})
}
return keyShape
},
setState(name, value, item) {
const size = [200, 56]
if (name === 'collapse') {
const group = item.getContainer()
const collapseText = group.find(e => e.get('name') === 'collapse-text')
if (collapseText) {
if (!value) {
collapseText.attr({
img: leftArrow,
x: size[0] / 2 - 6
})
} else {
collapseText.attr({
img: rightArrow,
x: size[0] / 2 - 5
})
}
}
}
}
})
// root node 类型节点
G6.registerNode('root', {
draw: (cfg, group) => {
const size = [180, 56]
const keyShape = group.addShape('rect', {
attrs: {
width: size[0],
height: size[1],
stroke: '#D7DAE2',
lineWidth: 0.5,
x: -size[0] / 2,
y: -size[1] / 2,
fill: '#fff',
radius: 4
},
draggable: true,
name: 'root-keyshape'
})
group.addShape('circle', {
attrs: {
x: -size[0] / 2,
y: 0,
stroke: '#D7DAE2',
lineWidth: 0.5,
r: 30,
fill: '#fff'
},
name: 'circle-bg'
})
group.addShape('circle', {
attrs: {
x: -size[0] / 2 + 7,
y: 0,
r: 28,
fill: '#fff'
},
name: 'circle-bg2'
})
group.addShape('circle', {
attrs: {
x: -size[0] / 2,
y: 0,
r: 26,
fill: '#EFF5FE'
},
name: 'circle-bg2'
})
group.addShape('image', {
attrs: {
x: -size[0] / 2 - 12,
y: -size[1] / 2 + 16,
width: 24,
height: 24,
img: root
},
name: 'image-shape'
})
group.addShape('text', {
attrs: {
text:
cfg.name.length > 10 ? cfg.name.substr(0, 10) + '...' : cfg.name,
fill: '#000',
fontSize: 10,
x: -size[0] / 2 + 35,
y: -5
},
cursor: 'pointer',
name: 'label-shape'
})
// collapse circle
if (cfg.children && cfg.children.length) {
group.addShape('circle', {
attrs: {
x: size[0] / 2,
y: 0,
stroke: '#D7DAE2',
lineWidth: 0.5,
r: 8,
cursor: 'pointer',
fill: '#fff'
},
name: 'collapse-back',
modelId: cfg.uid
})
// collpase text
group.addShape('image', {
attrs: {
x: size[0] / 2 - 5,
y: -6,
width: 12,
height: 12,
img: cfg.collapsed ? rightArrow : leftArrow,
cursor: 'pointer'
},
name: 'collapse-text',
modelId: cfg.uid
})
}
return keyShape
},
//设置收缩节点变化
setState(name, value, item) {
const size = [180, 56]
if (name === 'collapse') {
const group = item.getContainer()
const collapseText = group.find(e => e.get('name') === 'collapse-text')
if (collapseText) {
if (!value) {
collapseText.attr({
img: leftArrow,
x: size[0] / 2 - 6
})
} else {
collapseText.attr({
img: rightArrow,
x: size[0] / 2 - 5
})
}
}
}
}
})
}
数据格式
const defaultData = {
uid: '001',
id: '001',
version: 'v1',
group: 'apps',
images: ['nginx:1.7.9'],
name: 'xxxxx',
health: 'Healthy',
state: 'Running',
ago: '1 day',
children: [
{
uid: '002',
id: '002',
kind: 'Deployment',
version: 'v1',
group: 'apps',
namespace: 'default',
images: ['nginx:1.7.9'],
name: 'xxxxx',
health: 'Healthy',
state: 'Running',
ago: '1 day',
children: [
{
uid: '003',
id: '003',
kind: 'Pod',
version: 'v1',
group: 'apps',
namespace: 'default',
images: ['nginx:1.7.9'],
name: 'xxxxx',
health: 'Healthy',
state: 'Running',
ago: '2 days',
children: [
{
uid: '004',
id: '004',
kind: 'Pod',
version: 'v1',
group: 'apps',
namespace: 'default',
images: ['nginx:1.7.9'],
name: 'xxxxx',
health: 'Healthy',
state: 'Running',
ago: '45 minutes'
},
{
uid: '005',
id: '005',
kind: 'Pod',
version: 'v1',
group: 'apps',
namespace: 'default',
images: ['nginx:1.7.9'],
name: 'xxxxx',
health: 'Healthy',
state: 'Running',
ago: '59 seconds'
}
]
},
{
uid: '006',
id: '006',
kind: 'Pod',
version: 'v1',
group: 'apps',
namespace: 'default',
images: ['nginx:1.7.9'],
name: 'xxxxx',
health: 'Healthy',
state: 'Running',
ago: '10 months'
}
]
},
{
uid: '007',
id: '007',
kind: 'Deployment',
version: 'v1',
group: 'apps',
namespace: 'default',
images: ['nginx:1.7.9'],
name: 'xxxxx',
health: 'Healthy',
state: 'Running',
ago: '1 week'
}
]
}