|
@@ -1,15 +1,14 @@
|
|
|
<template>
|
|
|
- <div>
|
|
|
+ <div :class="['graph-container', { 'fullscreen': isFullscreen }]" ref="content">
|
|
|
<div class="graph-toolbar">
|
|
|
<button class="graph-btn" @click="zoomIn">放大</button>
|
|
|
<button class="graph-btn" @click="zoomOut">缩小</button>
|
|
|
<button class="graph-btn" @click="resetZoom">1:1</button>
|
|
|
+ <button class="graph-btn" @click="toggleFullscreen">{{ isFullscreen ? '退出全屏' : '全屏' }}</button>
|
|
|
<input v-model="searchText" @keyup.enter="searchNode" placeholder="搜索节点" class="graph-input" />
|
|
|
<button class="graph-btn graph-btn-primary" @click="searchNode">搜索</button>
|
|
|
</div>
|
|
|
- <div style="width: 100%; max-height: 600px; overflow: auto;padding-bottom: 10px;" class="content">
|
|
|
- <div style="height:600px;position: relative;" ref="container"></div>
|
|
|
- </div>
|
|
|
+ <div :style="{ height: isFullscreen ? '100vh' : '600px', position: 'relative' }" ref="container"></div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
@@ -22,6 +21,7 @@ export default {
|
|
|
return {
|
|
|
graph: null,
|
|
|
searchText: '',
|
|
|
+ isFullscreen: false,
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
@@ -31,9 +31,19 @@ export default {
|
|
|
}
|
|
|
});
|
|
|
window.addEventListener('resize', this.handleResize);
|
|
|
+ // 添加全屏事件监听
|
|
|
+ document.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
|
|
+ document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange);
|
|
|
+ document.addEventListener('mozfullscreenchange', this.handleFullscreenChange);
|
|
|
+ document.addEventListener('MSFullscreenChange', this.handleFullscreenChange);
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
window.removeEventListener('resize', this.handleResize);
|
|
|
+ // 移除全屏事件监听
|
|
|
+ document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
|
|
|
+ document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange);
|
|
|
+ document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange);
|
|
|
+ document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange);
|
|
|
if (this.graph) {
|
|
|
this.graph.destroy();
|
|
|
}
|
|
@@ -94,6 +104,96 @@ export default {
|
|
|
this.$message && this.$message.warning('未找到该节点');
|
|
|
}
|
|
|
},
|
|
|
+ toggleFullscreen() {
|
|
|
+ if (!this.isFullscreen) {
|
|
|
+ this.enterFullscreen();
|
|
|
+ } else {
|
|
|
+ this.exitFullscreen();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ enterFullscreen() {
|
|
|
+ const content = this.$refs.content;
|
|
|
+ if (content.requestFullscreen) {
|
|
|
+ content.requestFullscreen();
|
|
|
+ } else if (content.webkitRequestFullscreen) {
|
|
|
+ content.webkitRequestFullscreen();
|
|
|
+ } else if (content.mozRequestFullScreen) {
|
|
|
+ content.mozRequestFullScreen();
|
|
|
+ } else if (content.msRequestFullscreen) {
|
|
|
+ content.msRequestFullscreen();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ exitFullscreen() {
|
|
|
+ if (document.exitFullscreen) {
|
|
|
+ document.exitFullscreen();
|
|
|
+ } else if (document.webkitExitFullscreen) {
|
|
|
+ document.webkitExitFullscreen();
|
|
|
+ } else if (document.mozCancelFullScreen) {
|
|
|
+ document.mozCancelFullScreen();
|
|
|
+ } else if (document.msExitFullscreen) {
|
|
|
+ document.msExitFullscreen();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleFullscreenChange() {
|
|
|
+ this.isFullscreen = !!(
|
|
|
+ document.fullscreenElement ||
|
|
|
+ document.webkitFullscreenElement ||
|
|
|
+ document.mozFullScreenElement ||
|
|
|
+ document.msFullscreenElement
|
|
|
+ );
|
|
|
+
|
|
|
+ // 全屏状态改变时重新调整图形大小
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.handleResize();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 自定义分组布局函数
|
|
|
+ applyGroupLayout(data) {
|
|
|
+ const nodes = data.nodes;
|
|
|
+ // const edges = data.edges;
|
|
|
+
|
|
|
+ // 按类别分组节点
|
|
|
+ const categoryGroups = {};
|
|
|
+ nodes.forEach(node => {
|
|
|
+ const category = node.category || 'default';
|
|
|
+ if (!categoryGroups[category]) {
|
|
|
+ categoryGroups[category] = [];
|
|
|
+ }
|
|
|
+ categoryGroups[category].push(node);
|
|
|
+ });
|
|
|
+
|
|
|
+ const categories = Object.keys(categoryGroups);
|
|
|
+ const groupSpacing = 400; // 组间距离
|
|
|
+ const nodeSpacing = 80; // 组内节点距离
|
|
|
+
|
|
|
+ // 计算每个组的布局
|
|
|
+ categories.forEach((category, categoryIndex) => {
|
|
|
+ const groupNodes = categoryGroups[category];
|
|
|
+ const groupSize = Math.ceil(Math.sqrt(groupNodes.length));
|
|
|
+
|
|
|
+ // 计算组的中心位置
|
|
|
+ const groupCenterX = (categoryIndex % 3) * groupSpacing - groupSpacing;
|
|
|
+ const groupCenterY = Math.floor(categoryIndex / 3) * groupSpacing;
|
|
|
+
|
|
|
+ // 为组内每个节点分配位置
|
|
|
+ groupNodes.forEach((node, nodeIndex) => {
|
|
|
+ const row = Math.floor(nodeIndex / groupSize);
|
|
|
+ const col = nodeIndex % groupSize;
|
|
|
+
|
|
|
+ const x = groupCenterX + (col - groupSize / 2) * nodeSpacing;
|
|
|
+ const y = groupCenterY + (row - groupSize / 2) * nodeSpacing;
|
|
|
+
|
|
|
+ // 更新节点位置
|
|
|
+ const nodeData = data.nodes.find(n => n.id === node.id);
|
|
|
+ if (nodeData) {
|
|
|
+ nodeData.x = x;
|
|
|
+ nodeData.y = y;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ return data;
|
|
|
+ },
|
|
|
showSeeksGraph(data) {
|
|
|
if (this.graph) {
|
|
|
this.graph.destroy();
|
|
@@ -103,6 +203,9 @@ export default {
|
|
|
const container = this.$refs.container;
|
|
|
if (!container) return;
|
|
|
|
|
|
+ // 应用分组布局
|
|
|
+ const layoutData = this.applyGroupLayout(JSON.parse(JSON.stringify(data)));
|
|
|
+
|
|
|
this.graph = new G6.Graph({
|
|
|
container: container,
|
|
|
width: container.scrollWidth,
|
|
@@ -113,12 +216,13 @@ export default {
|
|
|
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
|
|
|
},
|
|
|
layout: {
|
|
|
- type: 'force',
|
|
|
- preventOverlap: true,
|
|
|
- nodeSpacing: 30,
|
|
|
- linkDistance: 200,
|
|
|
- nodeStrength: -60,
|
|
|
- edgeStrength: 0.1,
|
|
|
+ // type: 'force',
|
|
|
+ // preventOverlap: true,
|
|
|
+ // nodeSpacing: 30,
|
|
|
+ // linkDistance: 200,
|
|
|
+ // nodeStrength: -60,
|
|
|
+ // edgeStrength: 0.1,
|
|
|
+ type: 'preset', // 使用预设布局,使用我们计算的位置
|
|
|
},
|
|
|
defaultNode: {
|
|
|
size: 40,
|
|
@@ -220,11 +324,11 @@ export default {
|
|
|
});
|
|
|
|
|
|
|
|
|
- const nodeIds = new Set(data.nodes.map(node => node.id));
|
|
|
+ const nodeIds = new Set(layoutData.nodes.map(node => node.id));
|
|
|
|
|
|
// 统计每个节点的连线数量
|
|
|
const linkCount = {};
|
|
|
- data.edges.forEach(edge => {
|
|
|
+ layoutData.edges.forEach(edge => {
|
|
|
if (edge.source) linkCount[edge.source] = (linkCount[edge.source] || 0) + 1;
|
|
|
if (edge.target) linkCount[edge.target] = (linkCount[edge.target] || 0) + 1;
|
|
|
});
|
|
@@ -257,7 +361,7 @@ export default {
|
|
|
};
|
|
|
|
|
|
const graphData = {
|
|
|
- nodes: data.nodes.map(node => {
|
|
|
+ nodes: layoutData.nodes.map(node => {
|
|
|
const count = linkCount[node.id] || 0;
|
|
|
const size = 20 + count * 10; // 不封顶
|
|
|
const categoryColor = getCategoryColor(node.category);
|
|
@@ -271,13 +375,15 @@ export default {
|
|
|
? fullLabel.substring(0, maxLength) + '...'
|
|
|
: fullLabel;
|
|
|
|
|
|
- // 保留 comboId 字段
|
|
|
+ // 保留 comboId 字段和位置信息
|
|
|
const nodeData = {
|
|
|
id: node.id,
|
|
|
label: displayLabel, // 使用截断后的标签
|
|
|
fullLabel: fullLabel, // 保存完整标签用于tooltip
|
|
|
description: node.description,
|
|
|
size,
|
|
|
+ x: node.x, // 保留计算的位置
|
|
|
+ y: node.y, // 保留计算的位置
|
|
|
style: {
|
|
|
fill,
|
|
|
stroke: categoryColor,
|
|
@@ -287,7 +393,7 @@ export default {
|
|
|
if (node.comboId) nodeData.comboId = node.comboId;
|
|
|
return nodeData;
|
|
|
}),
|
|
|
- edges: data.edges
|
|
|
+ edges: layoutData.edges
|
|
|
.filter(edge => {
|
|
|
const sourceExists = nodeIds.has(edge.source);
|
|
|
const targetExists = nodeIds.has(edge.target);
|
|
@@ -301,7 +407,7 @@ export default {
|
|
|
target: edge.target,
|
|
|
label: edge.predicate,
|
|
|
})),
|
|
|
- combos: Array.isArray(data.combos) ? data.combos : undefined
|
|
|
+ combos: Array.isArray(layoutData.combos) ? layoutData.combos : undefined
|
|
|
};
|
|
|
console.log('graphData',graphData);
|
|
|
|
|
@@ -315,6 +421,7 @@ export default {
|
|
|
this.graph.on('node:mouseleave', (e) => {
|
|
|
this.graph.setItemState(e.item, 'focus', false);
|
|
|
});
|
|
|
+ this.handleResize();
|
|
|
},
|
|
|
handleResize() {
|
|
|
if (!this.graph || this.graph.get('destroyed')) return;
|
|
@@ -370,4 +477,12 @@ export default {
|
|
|
height: 30px;
|
|
|
outline: none;
|
|
|
}
|
|
|
+
|
|
|
+.graph-container{
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: #fff;
|
|
|
+ padding: 10px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
</style>
|