app.vue
<template>
<div class="graph-container">
<div id="graph" style="width: 1000px; height: 600px;"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import G6 from '@antv/g6';
import DoubleRectNode from './components/DoubleRectNode.vue';
import CenterNode from './components/CenterNode.vue';
import RightRectNode from './components/RightRectNode.vue';
onMounted(() => {
const graphData = {
nodes: [
{ id: 'nodeA', label: 'A', subLabel: '1', type: 'double-rect' },
{ id: 'nodeB', label: 'B', subLabel: '2', type: 'double-rect' },
{ id: 'nodeC', label: 'C', subLabel: '3', type: 'double-rect' },
{ id: 'center', label: '进入', type: 'center-node' },
{ id: 'right1', label: '8k', type: 'right-rect' },
{ id: 'right2', label: '8k', type: 'right-rect' },
{ id: 'right3', label: '1k', type: 'right-rect' },
{ id: 'right4', label: '1k', type: 'right-rect' },
{ id: 'right5', label: '1k', type: 'right-rect' },
],
edges: [
{ source: 'nodeA', target: 'center' },
{ source: 'nodeB', target: 'center' },
{ source: 'nodeC', target: 'center' },
{ source: 'center', target: 'right1' },
{ source: 'center', target: 'right2' },
{ source: 'center', target: 'right3' },
{ source: 'center', target: 'right4' },
{ source: 'center', target: 'right5' },
],
};
// 注册自定义节点渲染器
G6.registerNode(
'double-rect',
{
draw(cfg, group) {
const vnode = h(DoubleRectNode, {
x: 0,
y: 0,
label: cfg.label,
subLabel: cfg.subLabel
});
const component = render(vnode, group);
return group;
}
},
'single-node'
);
G6.registerNode(
'center-node',
{
draw(cfg, group) {
const vnode = h(CenterNode, {
x: 0,
y: 0,
label: cfg.label
});
const component = render(vnode, group);
return group;
}
},
'single-node'
);
G6.registerNode(
'right-rect',
{
draw(cfg, group) {
const vnode = h(RightRectNode, {
x: 0,
y: 0,
label: cfg.label
});
const component = render(vnode, group);
return group;
}
},
'single-node'
);
const graph = new G6.TreeGraph({
container: 'graph',
width: 1000,
height: 600,
layout: {
type: 'tree',
direction: 'LR',
depth: 1,
nodeSep: 80,
rankSep: 120,
},
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
},
});
graph.data(graphData);
graph.render();
return () => {
graph.destroy();
};
});
</script>
<style scoped>
.graph-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
组件left
<template>
<g>
<rect :x="x" :y="y - 25" :width="width" :height="20" :fill="fillTop" :stroke="stroke" :rx="radius" :ry="radius"/>
<rect :x="x" :y="y + 5" :width="width" :height="20" :fill="fillBottom" :stroke="stroke" :rx="radius" :ry="radius"/>
<text :x="x + width / 2" :y="y - 15" :fill="textFillTop" :font-size="textSize" :text-anchor="textAnchor">{{ label }}</text>
<text :x="x + width / 2" :y="y + 15" :fill="textFillBottom" :font-size="textSize" :text-anchor="textAnchor">{{ subLabel }}</text>
</g>
</template>
<script setup>
const props = defineProps({
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
},
width: {
type: Number,
default: 80
},
fillTop: {
type: String,
default: '#f5f5f5'
},
fillBottom: {
type: String,
default: '#fff'
},
stroke: {
type: String,
default: '#999'
},
radius: {
type: Number,
default: 2
},
label: {
type: String,
required: true
},
subLabel: {
type: String,
required: true
},
textFillTop: {
type: String,
default: '#333'
},
textFillBottom: {
type: String,
default: '#666'
},
textSize: {
type: Number,
default: 12
},
textAnchor: {
type: String,
default: 'center'
}
});
</script>
right
<template>
<g>
<rect :x="x - 60" :y="y - 10" :width="width" :height="20" :fill="fill" :stroke="stroke" :rx="radius" :ry="radius"/>
<text :x="x - 50" :y="y + 5" :fill="textFill" :font-size="textSize">{{ label }}</text>
</g>
</template>
<script setup>
const props = defineProps({
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
},
width: {
type: Number,
default: 120
},
fill: {
type: String,
default: '#f9f9f9'
},
stroke: {
type: String,
default: '#999'
},
radius: {
type: Number,
default: 2
},
label: {
type: String,
required: true
},
textFill: {
type: String,
default: '#666'
},
textSize: {
type: Number,
default: 12
}
});
</script>
center
<template>
<g>
<rect :x="x - 30" :y="y - 15" :width="60" :height="30" :fill="fill" :stroke="stroke" :rx="radius" :ry="radius"/>
<text :x="x" :y="y + 5" :fill="textFill" :font-size="textSize" :text-anchor="textAnchor">{{ label }}</text>
</g>
</template>
<script setup>
const props = defineProps({
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
},
fill: {
type: String,
default: '#fff'
},
stroke: {
type: String,
default: '#000'
},
radius: {
type: Number,
default: 4
},
label: {
type: String,
required: true
},
textFill: {
type: String,
default: '#333'
},
textSize: {
type: Number,
default: 14
},
textAnchor: {
type: String,
default: 'center'
}
});
</script>
容器
<template>
<div class="graph-container" ref="container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import G6 from '@antv/g6';
import LeftNode from './nodes/LeftNode.vue';
import CenterNode from './nodes/CenterNode.vue';
import RightNode from './nodes/RightNode.vue';
// 接收父组件传入的数据
const props = defineProps({
nodes: {
type: Array,
required: true
},
edges: {
type: Array,
required: true
}
});
const container = ref(null);
let graph = null;
// 初始化图表
const initGraph = () => {
// 销毁现有图表实例
if (graph) {
graph.destroy();
}
// 注册自定义节点
G6.registerNode('left-node', {
draw: LeftNode.draw,
update: LeftNode.update
}, 'single-node');
G6.registerNode('center-node', {
draw: CenterNode.draw,
update: CenterNode.update
}, 'single-node');
G6.registerNode('right-node', {
draw: RightNode.draw,
update: RightNode.update
}, 'single-node');
// 创建图表实例
graph = new G6.Graph({
container: container.value,
width: container.value.clientWidth,
height: container.value.clientHeight,
layout: {
type: 'grid',
cols: 3,
rowGap: 40,
colGap: 200,
sortBy: node => {
if (node.data.type === 'left') return 1;
if (node.data.type === 'center') return 2;
return 3;
}
},
defaultEdge: {
type: 'line',
style: {
stroke: '#999',
lineWidth: 2,
endArrow: true
}
},
modes: {
default: ['drag-node', 'zoom-canvas', 'drag-canvas']
}
});
// 处理节点类型
const nodesWithType = props.nodes.map(node => {
let type;
if (node.type === 'left') type = 'left-node';
else if (node.type === 'right') type = 'right-node';
else type = 'center-node';
return { ...node, type };
});
// 加载数据并渲染
graph.data({
nodes: nodesWithType,
edges: props.edges
});
graph.render();
// 添加节点点击事件
graph.on('node:click', (e) => {
const node = e.item;
graph.setItemState(node, 'active', true);
// 高亮关联边
graph.getEdges().forEach(edge => {
const isRelated = edge.getSource().getID() === node.getID() ||
edge.getTarget().getID() === node.getID();
graph.setItemState(edge, 'active', isRelated);
});
});
// 画布点击取消高亮
graph.on('canvas:click', () => {
graph.getNodes().forEach(node => graph.setItemState(node, 'active', false));
graph.getEdges().forEach(edge => graph.setItemState(edge, 'active', false));
});
// 高亮样式
graph.nodeStateStyles.active = {
shadowBlur: 15,
shadowColor: 'rgba(0,0,0,0.3)'
};
graph.edgeStateStyles.active = {
stroke: '#fa541c',
lineWidth: 3
};
};
// 响应窗口大小变化
const handleResize = () => {
if (graph) {
graph.changeSize(
container.value.clientWidth,
container.value.clientHeight
);
}
};
// 监听数据变化,更新图表
watch([() => props.nodes, () => props.edges], () => {
if (graph) {
const nodesWithType = props.nodes.map(node => {
let type;
if (node.type === 'left') type = 'left-node';
else if (node.type === 'right') type = 'right-node';
else type = 'center-node';
return { ...node, type };
});
graph.data({
nodes: nodesWithType,
edges: props.edges
});
graph.render();
}
}, { deep: true });
onMounted(() => {
initGraph();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
if (graph) {
graph.destroy();
}
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
.graph-container {
width: 100%;
height: calc(100% - 60px);
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
all
<template>
<div class="graph-container">
<!-- G6 挂载容器 -->
<div id="graph" style="width: 1000px; height: 600px;"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import G6 from '@antv/g6';
onMounted(() => {
// 1. 初始化数据(与手绘结构对应)
const graphData = {
nodes: [
// 左侧节点(A-1、B-2、C-3)
{ id: 'nodeA', label: 'A', subLabel: '1', type: 'double-rect' },
{ id: 'nodeB', label: 'B', subLabel: '2', type: 'double-rect' },
{ id: 'nodeC', label: 'C', subLabel: '3', type: 'double-rect' },
// 中间核心节点
{ id: 'center', label: '进入', type: 'center-node' },
// 右侧节点(带 8k/1k 标记)
{ id: 'right1', label: '8k', type: 'right-rect' },
{ id: 'right2', label: '8k', type: 'right-rect' },
{ id: 'right3', label: '1k', type: 'right-rect' },
{ id: 'right4', label: '1k', type: 'right-rect' },
{ id: 'right5', label: '1k', type: 'right-rect' },
],
edges: [
// 左侧节点连中间
{ source: 'nodeA', target: 'center' },
{ source: 'nodeB', target: 'center' },
{ source: 'nodeC', target: 'center' },
// 中间连右侧
{ source: 'center', target: 'right1' },
{ source: 'center', target: 'right2' },
{ source: 'center', target: 'right3' },
{ source: 'center', target: 'right4' },
{ source: 'center', target: 'right5' },
],
};
// 2. 注册自定义节点(左侧双层矩形)
G6.registerNode(
'double-rect',
{
draw(cfg, group) {
// 上层矩形
const rectTop = group.addShape('rect', {
attrs: {
x: -40,
y: -25,
width: 80,
height: 20,
fill: '#f5f5f5',
stroke: '#999',
radius: 2,
},
});
// 下层矩形
const rectBottom = group.addShape('rect', {
attrs: {
x: -40,
y: 5,
width: 80,
height: 20,
fill: '#fff',
stroke: '#999',
radius: 2,
},
});
// 上层文字(A/B/C)
group.addShape('text', {
attrs: {
x: 0,
y: -15,
text: cfg.label,
fill: '#333',
fontSize: 12,
textAlign: 'center',
},
});
// 下层文字(1/2/3)
group.addShape('text', {
attrs: {
x: 0,
y: 15,
text: cfg.subLabel,
fill: '#666',
fontSize: 12,
textAlign: 'center',
},
});
return group;
},
},
'single-node', // 继承基础节点
);
// 3. 注册中间核心节点(进入)
G6.registerNode(
'center-node',
{
draw(cfg, group) {
const rect = group.addShape('rect', {
attrs: {
x: -30,
y: -15,
width: 60,
height: 30,
fill: '#fff',
stroke: '#000',
radius: 4,
},
});
group.addShape('text', {
attrs: {
x: 0,
y: 5,
text: cfg.label,
fill: '#333',
fontSize: 14,
textAlign: 'center',
},
});
return group;
},
},
'single-node',
);
// 4. 注册右侧带长度标记的节点(8k/1k)
G6.registerNode(
'right-rect',
{
draw(cfg, group) {
const rect = group.addShape('rect', {
attrs: {
x: -60,
y: -10,
width: 120, // 长度根据需求调整
height: 20,
fill: '#f9f9f9',
stroke: '#999',
radius: 2,
},
});
group.addShape('text', {
attrs: {
x: -50, // 文字靠左,留空间体现长度
y: 5,
text: cfg.label,
fill: '#666',
fontSize: 12,
},
});
return group;
},
},
'single-node',
);
// 5. 初始化 G6 图实例
const graph = new G6.TreeGraph({
container: 'graph',
width: 1000,
height: 600,
// 树布局配置:左侧节点作为根,向右布局
layout: {
type: 'tree',
direction: 'LR', // 从左到右布局
depth: 1,
nodeSep: 80, // 节点间距
rankSep: 120, // 层间距
},
modes: {
default: ['drag-canvas', 'zoom-canvas', 'drag-node'], // 交互模式
},
});
// 6. 加载数据并渲染
graph.data(graphData);
graph.render();
return () => {
// 组件销毁时销毁图实例
graph.destroy();
};
});
</script>
<style scoped>
.graph-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
使用
<template>
<div class="app-container">
<h1>算法分类思维导图</h1>
<G6Mindmap />
</div>
</template>
<script setup>
import G6Mindmap from './components/G6Mindmap.vue';
</script>
2904

被折叠的 条评论
为什么被折叠?



