|
@@ -3,10 +3,12 @@
|
|
<el-card class="full-width-card">
|
|
<el-card class="full-width-card">
|
|
<div class="toolbar">
|
|
<div class="toolbar">
|
|
<p>三维图数据可视化</p>
|
|
<p>三维图数据可视化</p>
|
|
- <el-button @click="resetCamera">对正视角</el-button>
|
|
|
|
|
|
+ <el-button type="primary" style="margin-left: auto;" @click="resetCamera">对正视角</el-button>
|
|
|
|
+ <el-button type="primary" @click="captureView">保存视图</el-button>
|
|
</div>
|
|
</div>
|
|
- <div class="graph-container">
|
|
|
|
- <Renderer ref="renderer" antialias orbit-ctrl resize :alpha="true" :background-alpha="0">
|
|
|
|
|
|
+ <div class="graph-container" ref="graphContainer">
|
|
|
|
+ <Renderer ref="renderer" antialias orbit-ctrl resize :alpha="true" :background-alpha="0"
|
|
|
|
+ :renderer="{ preserveDrawingBuffer: true }">
|
|
<Camera :position="initialCameraPos" />
|
|
<Camera :position="initialCameraPos" />
|
|
<Scene ref="scene">
|
|
<Scene ref="scene">
|
|
<!-- 使用空组件的 ref 绑定场景 -->
|
|
<!-- 使用空组件的 ref 绑定场景 -->
|
|
@@ -15,85 +17,336 @@
|
|
</Renderer>
|
|
</Renderer>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-card>
|
|
|
|
+ <div v-for="(info, index) in infoBoxes" :key="info.id" class="info-box" :style="info.position">
|
|
|
|
+ <div class="close-btn" @click="closeInfoBox(info.id)">×</div>
|
|
|
|
+ <div class="content">节点ID: {{ info.nodeId }}</div>
|
|
|
|
+ </div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<script setup>
|
|
<script setup>
|
|
-import { onMounted, ref, watch, nextTick, onBeforeUnmount } from 'vue';
|
|
|
|
|
|
+import { onMounted, ref, reactive, watch, nextTick, onBeforeUnmount } from 'vue';
|
|
|
|
+import { saveAs } from 'file-saver';
|
|
import { postData, getData } from '@/api/axios.js'
|
|
import { postData, getData } from '@/api/axios.js'
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
import { Renderer, Camera, Scene, Group } from 'troisjs';
|
|
import { Renderer, Camera, Scene, Group } from 'troisjs';
|
|
import { MeshBasicMaterial } from 'three';
|
|
import { MeshBasicMaterial } from 'three';
|
|
import * as THREE from 'three';
|
|
import * as THREE from 'three';
|
|
|
|
+import html2canvas from 'html2canvas';
|
|
|
|
+
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
result: {
|
|
result: {
|
|
type: Number,
|
|
type: Number,
|
|
required: true,
|
|
required: true,
|
|
}
|
|
}
|
|
})
|
|
})
|
|
-// 加载中占位符
|
|
|
|
|
|
+// 状态管理
|
|
const loading = ref(true)
|
|
const loading = ref(true)
|
|
const nodes = ref([])
|
|
const nodes = ref([])
|
|
const edges = ref([])
|
|
const edges = ref([])
|
|
const lines = ref([])
|
|
const lines = ref([])
|
|
const renderer = ref(null)
|
|
const renderer = ref(null)
|
|
|
|
+const scene = ref(null)
|
|
|
|
+const nodesGroup = ref(null)
|
|
|
|
+const infoBoxes = ref([])
|
|
|
|
+const selectedNodes = ref(new Set())
|
|
|
|
+const connectionLines = ref([])
|
|
|
|
+
|
|
|
|
+// 三维相关
|
|
|
|
+const raycaster = new THREE.Raycaster()
|
|
|
|
+const mouse = new THREE.Vector2()
|
|
const initialCameraPos = ref({ x: 0, y: 0, z: 1 })
|
|
const initialCameraPos = ref({ x: 0, y: 0, z: 1 })
|
|
|
|
+const sceneBoundingSphere = new THREE.Sphere()
|
|
|
|
+
|
|
|
|
+// 相机初始状态
|
|
const initialCameraState = ref({
|
|
const initialCameraState = ref({
|
|
position: new THREE.Vector3(),
|
|
position: new THREE.Vector3(),
|
|
target: new THREE.Vector3()
|
|
target: new THREE.Vector3()
|
|
-});
|
|
|
|
-const cameraCenter = ref({})
|
|
|
|
-const cameraDistance = ref(null)
|
|
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+// 显示视图容器尺寸监控
|
|
|
|
+const graphContainer = ref(null)
|
|
|
|
+const containerRect = ref({
|
|
|
|
+ left: 0,
|
|
|
|
+ top: 0,
|
|
|
|
+ width: 1,
|
|
|
|
+ height: 1
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+// 信息框布局逻辑
|
|
|
|
+const layoutInfo = reactive({
|
|
|
|
+ left: [],
|
|
|
|
+ right: []
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+// 计算信息框位置
|
|
|
|
+const getBoxPosition = () => {
|
|
|
|
+ const container = graphContainer.value
|
|
|
|
+ const containerRect = container.getBoundingClientRect()
|
|
|
|
+ const boxHeight = 80 // 信息框高度+间距
|
|
|
|
+ const maxVisible = Math.floor((containerRect.height - 50) / boxHeight)
|
|
|
|
+
|
|
|
|
+ // 优先填充左侧
|
|
|
|
+ let side = 'left'
|
|
|
|
+ if (layoutInfo.left.length >= maxVisible) {
|
|
|
|
+ side = 'right'
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 计算新位置
|
|
|
|
+ const baseY = 50 + layoutInfo[side].length * boxHeight
|
|
|
|
+ layoutInfo[side].push(baseY)
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ side,
|
|
|
|
+ style: {
|
|
|
|
+ [side === 'left' ? 'left' : 'right']: '20px',
|
|
|
|
+ top: `${baseY}px`
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
|
|
-// 场景对象
|
|
|
|
-const scene = ref(null);
|
|
|
|
-const nodesGroup = ref(null);
|
|
|
|
-let sceneBoundingSphere = new THREE.Sphere(); // 场景包围球
|
|
|
|
|
|
+const updateContainerRect = () => {
|
|
|
|
+ if (!graphContainer.value) return
|
|
|
|
+ const rect = graphContainer.value.getBoundingClientRect()
|
|
|
|
+ containerRect.value = {
|
|
|
|
+ left: rect.left,
|
|
|
|
+ top: rect.top,
|
|
|
|
+ width: rect.width,
|
|
|
|
+ height: rect.height
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 用于显示的各种颜色
|
|
|
|
+const COLORS = {
|
|
|
|
+ NODE_DEFAULT: 0x0000FF, // 蓝色
|
|
|
|
+ NODE_SELECTED: 0xFFA500,
|
|
|
|
+ NODE_HOVER: 0x00FF00, // 绿色
|
|
|
|
+ NODE_RELATED: 0xFFD700, // 金色
|
|
|
|
+ EDGE_DEFAULT: 0x800080, // 紫色
|
|
|
|
+ EDGE_HOVER: 0xFFA500 // 橙色
|
|
|
|
+}
|
|
|
|
|
|
|
|
+// 用于透明化的样式
|
|
|
|
+const STYLE = {
|
|
|
|
+ HIGHLIGHT_OPACITY: 1.0,
|
|
|
|
+ DIM_OPACITY: 0.05,
|
|
|
|
+ NODE_COLOR: 0x0000FF,
|
|
|
|
+ EDGE_COLOR: 0x800080,
|
|
|
|
+ HOVER_NODE_COLOR: 0x00FF00,
|
|
|
|
+ RELATED_NODE_COLOR: 0xFFD700,
|
|
|
|
+ HOVER_EDGE_COLOR: 0xFFA500
|
|
|
|
+}
|
|
|
|
|
|
-// 动态创建边的函数
|
|
|
|
|
|
+// 动态创建节点与边的函数
|
|
const createEdges = () => {
|
|
const createEdges = () => {
|
|
- // 创建节点
|
|
|
|
// 清理旧对象
|
|
// 清理旧对象
|
|
if (nodesGroup.value?.group) {
|
|
if (nodesGroup.value?.group) {
|
|
nodesGroup.value.group.children = [];
|
|
nodesGroup.value.group.children = [];
|
|
}
|
|
}
|
|
|
|
|
|
- // 创建蓝色节点
|
|
|
|
|
|
+ // 创建节点
|
|
nodes.value.forEach(node => {
|
|
nodes.value.forEach(node => {
|
|
- const geometry = new THREE.SphereGeometry(0.2);
|
|
|
|
- const material = new THREE.MeshBasicMaterial({ color: 0x0000FF });
|
|
|
|
- const sphere = new THREE.Mesh(geometry, material);
|
|
|
|
- sphere.position.set(
|
|
|
|
- node.coordinates.x,
|
|
|
|
- node.coordinates.y,
|
|
|
|
- node.coordinates.z
|
|
|
|
- );
|
|
|
|
- nodesGroup.value.group.add(sphere);
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- //创建边
|
|
|
|
- lines.value.forEach(line => scene.value.scene.remove(line));
|
|
|
|
- lines.value = [];
|
|
|
|
|
|
+ const geometry = new THREE.SphereGeometry(0.2)
|
|
|
|
+ const material = new THREE.MeshBasicMaterial({
|
|
|
|
+ color: STYLE.NODE_COLOR,
|
|
|
|
+ transparent: true,
|
|
|
|
+ opacity: STYLE.HIGHLIGHT_OPACITY,
|
|
|
|
+ })
|
|
|
|
+ const sphere = new THREE.Mesh(geometry, material)
|
|
|
|
+ sphere.position.set(...Object.values(node.coordinates))
|
|
|
|
+ sphere.userData = { type: 'node', id: node.id }
|
|
|
|
+ nodesGroup.value.group.add(sphere)
|
|
|
|
+ })
|
|
|
|
|
|
|
|
+ // 创建边(添加鼠标事件支持)
|
|
|
|
+ lines.value.forEach(line => scene.value.scene.remove(line))
|
|
|
|
+ lines.value = []
|
|
edges.value.forEach(edge => {
|
|
edges.value.forEach(edge => {
|
|
- const points = edge.map(p => new THREE.Vector3(p.x, p.y, p.z));
|
|
|
|
- const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
|
|
|
|
+ const points = edge.map(p => new THREE.Vector3(...Object.values(p.coordinates)))
|
|
|
|
+ const geometry = new THREE.BufferGeometry().setFromPoints(points)
|
|
|
|
+ const material = new THREE.LineBasicMaterial({
|
|
|
|
+ color: STYLE.EDGE_COLOR,
|
|
|
|
+ linewidth: 2,
|
|
|
|
+ transparent: true,
|
|
|
|
+ opacity: STYLE.HIGHLIGHT_OPACITY
|
|
|
|
+ })
|
|
|
|
+ const line = new THREE.Line(geometry, material)
|
|
|
|
+ line.userData = {
|
|
|
|
+ type: 'edge',
|
|
|
|
+ from: edge[0].id, // 假设edge数据包含节点ID
|
|
|
|
+ to: edge[1].id // 需要根据实际数据结构调整
|
|
|
|
+ }
|
|
|
|
+ scene.value.scene.add(line)
|
|
|
|
+ lines.value.push(line)
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 计算包围球
|
|
|
|
+ const box = new THREE.Box3().setFromObject(nodesGroup.value.group)
|
|
|
|
+ box.getBoundingSphere(sceneBoundingSphere)
|
|
|
|
+ // 动态设置相机初始位置
|
|
|
|
+ const aspect = window.innerWidth / window.innerHeight
|
|
|
|
+ initialCameraPos.value.z = sceneBoundingSphere.radius * 2.5 * (aspect > 1 ? 1 : 1.5)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+// 鼠标悬浮时高亮描边
|
|
|
|
+const handleHover = (event) => {
|
|
|
|
+ if (!graphContainer.value) return
|
|
|
|
+ updateContainerRect()
|
|
|
|
|
|
- const material = new THREE.LineBasicMaterial({ color: 0x800080 });
|
|
|
|
|
|
+ // 重置所有元素的样式
|
|
|
|
+ nodesGroup.value.group.children.forEach(child => {
|
|
|
|
+ if (!selectedNodes.value.has(child.userData.id)) {
|
|
|
|
+ child.material.color.set(COLORS.NODE_DEFAULT)
|
|
|
|
+ }
|
|
|
|
+ child.material.opacity = STYLE.HIGHLIGHT_OPACITY
|
|
|
|
+ })
|
|
|
|
+ lines.value.forEach(line => {
|
|
|
|
+ line.material.color.set(STYLE.EDGE_COLOR)
|
|
|
|
+ line.material.opacity = STYLE.HIGHLIGHT_OPACITY
|
|
|
|
+ })
|
|
|
|
|
|
- const line = new THREE.Line(geometry, material);
|
|
|
|
- scene.value.scene.add(line);
|
|
|
|
- lines.value.push(line);
|
|
|
|
|
|
+ // 计算标准化设备坐标
|
|
|
|
+ const x = (event.clientX - containerRect.value.left) / containerRect.value.width * 2 - 1
|
|
|
|
+ const y = -(event.clientY - containerRect.value.top) / containerRect.value.height * 2 + 1
|
|
|
|
+
|
|
|
|
+ // 射线检测
|
|
|
|
+ raycaster.setFromCamera(new THREE.Vector2(x, y), renderer.value.camera)
|
|
|
|
+ const intersects = raycaster.intersectObjects(nodesGroup.value.group.children)
|
|
|
|
+
|
|
|
|
+ if (intersects.length > 0) {
|
|
|
|
+ const currentNode = intersects[0].object
|
|
|
|
+ const relatedNodes = new Set()
|
|
|
|
+ const relatedEdges = []
|
|
|
|
+
|
|
|
|
+ // 查找相关边和节点
|
|
|
|
+ lines.value.forEach(line => {
|
|
|
|
+ const edgeData = line.userData
|
|
|
|
+ if (edgeData.from === currentNode.userData.id || edgeData.to === currentNode.userData.id) {
|
|
|
|
+ relatedEdges.push(line)
|
|
|
|
+ const relatedId = edgeData.from === currentNode.userData.id ? edgeData.to : edgeData.from
|
|
|
|
+ relatedNodes.add(relatedId)
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 设置高亮样式
|
|
|
|
+ if (!selectedNodes.value.has(currentNode.userData.id)) {
|
|
|
|
+ currentNode.material.color.set(STYLE.HOVER_NODE_COLOR)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 高亮相关边
|
|
|
|
+ // relatedEdges.forEach(line => {
|
|
|
|
+ // line.material.color.set(STYLE.HOVER_EDGE_COLOR)
|
|
|
|
+ // line.material.opacity = STYLE.HIGHLIGHT_OPACITY
|
|
|
|
+ // })
|
|
|
|
+
|
|
|
|
+ // // 高亮相关节点
|
|
|
|
+ // nodesGroup.value.group.children.forEach(node => {
|
|
|
|
+ // if (relatedNodes.has(node.userData.id)) {
|
|
|
|
+ // node.material.color.set(STYLE.RELATED_NODE_COLOR)
|
|
|
|
+ // node.material.opacity = STYLE.HIGHLIGHT_OPACITY
|
|
|
|
+ // }
|
|
|
|
+ // })
|
|
|
|
+
|
|
|
|
+ // 淡化无关元素
|
|
|
|
+ nodesGroup.value.group.children.forEach(node => {
|
|
|
|
+ if (node !== currentNode && !relatedNodes.has(node.userData.id)) {
|
|
|
|
+ node.material.opacity = STYLE.DIM_OPACITY
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ lines.value.forEach(line => {
|
|
|
|
+ if (!relatedEdges.includes(line)) {
|
|
|
|
+ line.material.opacity = STYLE.DIM_OPACITY
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 用于生成信息框ID
|
|
|
|
+const generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2)
|
|
|
|
+
|
|
|
|
+// 添加信息框显示
|
|
|
|
+const addInfoBox = (node, nodeId) => {
|
|
|
|
+ const position = getBoxPosition();
|
|
|
|
+
|
|
|
|
+ infoBoxes.value.push({
|
|
|
|
+ id: generateId(),
|
|
|
|
+ nodeId,
|
|
|
|
+ position: position.style,
|
|
|
|
+ side: position.side
|
|
});
|
|
});
|
|
|
|
|
|
- // 计算包围球
|
|
|
|
- const box = new THREE.Box3().setFromObject(nodesGroup.value.group);
|
|
|
|
- box.getBoundingSphere(sceneBoundingSphere);
|
|
|
|
- // 动态设置相机初始位置
|
|
|
|
- const aspect = window.innerWidth / window.innerHeight;
|
|
|
|
- initialCameraPos.value.z = sceneBoundingSphere.radius * 2.5 * (aspect > 1 ? 1 : 1.5);
|
|
|
|
|
|
+ // 更新节点状态
|
|
|
|
+ selectedNodes.value.add(nodeId);
|
|
|
|
+ node.material.color.set(COLORS.NODE_SELECTED);
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+// 移除信息框显示
|
|
|
|
+
|
|
|
|
+const removeInfoBox = (nodeId) => {
|
|
|
|
+ const removedBox = infoBoxes.value.find(b => b.nodeId == nodeId)
|
|
|
|
+ closeInfoBox(removedBox.id)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const closeInfoBox = (boxId) => {
|
|
|
|
+ const boxIndex = infoBoxes.value.findIndex(b => b.id === boxId)
|
|
|
|
+ if (boxIndex === -1) return
|
|
|
|
+
|
|
|
|
+ // 移除信息框并重置布局计数器
|
|
|
|
+ const removedBox = infoBoxes.value.splice(boxIndex, 1)[0]
|
|
|
|
+ // 从布局数组中移除位置
|
|
|
|
+ const posIndex = layoutInfo[removedBox.side].indexOf(parseInt(removedBox.position.top))
|
|
|
|
+ if (posIndex > -1) {
|
|
|
|
+ layoutInfo[removedBox.side].splice(posIndex, 1)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 重新排列同侧信息框
|
|
|
|
+ infoBoxes.value
|
|
|
|
+ .filter(b => b.side === removedBox.side)
|
|
|
|
+ .forEach((box, index) => {
|
|
|
|
+ const newTop = 50 + index * 80
|
|
|
|
+ box.position.top = `${newTop}px`
|
|
|
|
+ layoutInfo[removedBox.side][index] = newTop
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ selectedNodes.value.delete(removedBox.nodeId)
|
|
|
|
+
|
|
|
|
+ const targetNode = nodesGroup.value.group.children.find(
|
|
|
|
+ n => n.userData.id === removedBox.nodeId
|
|
|
|
+ )
|
|
|
|
+ if (targetNode) {
|
|
|
|
+ targetNode.material.color.set(COLORS.NODE_DEFAULT)
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleClick = (event) => {
|
|
|
|
+ if (!graphContainer.value) return
|
|
|
|
+ updateContainerRect()
|
|
|
|
+
|
|
|
|
+ // 计算标准化设备坐标
|
|
|
|
+ const x = (event.clientX - containerRect.value.left) / containerRect.value.width * 2 - 1
|
|
|
|
+ const y = -(event.clientY - containerRect.value.top) / containerRect.value.height * 2 + 1
|
|
|
|
+
|
|
|
|
+ raycaster.setFromCamera(new THREE.Vector2(x, y), renderer.value.camera)
|
|
|
|
+ const intersects = raycaster.intersectObjects([
|
|
|
|
+ ...nodesGroup.value.group.children
|
|
|
|
+ ])
|
|
|
|
+
|
|
|
|
+ if (intersects.length > 0) {
|
|
|
|
+ const obj = intersects[0].object
|
|
|
|
+ const nodeId = obj.userData.id
|
|
|
|
+
|
|
|
|
+ if (selectedNodes.value.has(nodeId)) {
|
|
|
|
+ removeInfoBox(nodeId)
|
|
|
|
+ } else {
|
|
|
|
+ addInfoBox(obj, nodeId)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|
|
const handleResize = () => {
|
|
const handleResize = () => {
|
|
nextTick(() => {
|
|
nextTick(() => {
|
|
if (renderer.value?.renderer) {
|
|
if (renderer.value?.renderer) {
|
|
@@ -107,7 +360,7 @@ const handleResize = () => {
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
-// 修正重置函数
|
|
|
|
|
|
+// 重置视角函数
|
|
const resetCamera = () => {
|
|
const resetCamera = () => {
|
|
console.log(initialCameraState.value.position)
|
|
console.log(initialCameraState.value.position)
|
|
|
|
|
|
@@ -121,18 +374,61 @@ const resetCamera = () => {
|
|
renderer.value.camera.lookAt(initialCameraState.value.target);
|
|
renderer.value.camera.lookAt(initialCameraState.value.target);
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+// 截图视图函数
|
|
|
|
+const captureView = async () => {
|
|
|
|
+ // 渲染最终帧
|
|
|
|
+ renderer.value.renderer.render(scene.value.scene, renderer.value.camera);
|
|
|
|
+
|
|
|
|
+ // 获取基础画布
|
|
|
|
+ const mainCanvas = renderer.value.renderer.domElement;
|
|
|
|
+ const finalCanvas = document.createElement('canvas');
|
|
|
|
+ const ctx = finalCanvas.getContext('2d');
|
|
|
|
+
|
|
|
|
+ // 设置画布尺寸
|
|
|
|
+ finalCanvas.width = mainCanvas.width;
|
|
|
|
+ finalCanvas.height = mainCanvas.height;
|
|
|
|
+
|
|
|
|
+ // 绘制三维内容
|
|
|
|
+ ctx.drawImage(mainCanvas, 0, 0);
|
|
|
|
+
|
|
|
|
+ // 绘制信息框
|
|
|
|
+ await Promise.all(infoBoxes.value.map(async (info, index) => {
|
|
|
|
+ const box = document.querySelector(`.info-box:nth-child(${index + 2})`);
|
|
|
|
+ const boxCanvas = await html2canvas(box);
|
|
|
|
+ ctx.drawImage(boxCanvas,
|
|
|
|
+ box.offsetLeft * window.devicePixelRatio,
|
|
|
|
+ box.offsetTop * window.devicePixelRatio
|
|
|
|
+ );
|
|
|
|
+ }));
|
|
|
|
+
|
|
|
|
+ // 保存结果
|
|
|
|
+ finalCanvas.toBlob(blob => {
|
|
|
|
+ saveAs(blob, `visualization_${Date.now()}.png`);
|
|
|
|
+ });
|
|
|
|
+};
|
|
|
|
+
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
window.addEventListener('resize', handleResize);
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
+ window.addEventListener('mousemove', handleHover);
|
|
|
|
+ window.addEventListener('click', handleClick);
|
|
|
|
+ // 用于监控视图容器变化
|
|
|
|
+ window.addEventListener('resize', updateContainerRect)
|
|
|
|
+ window.addEventListener('scroll', updateContainerRect, true)
|
|
// 根据result的id获取图像结果
|
|
// 根据result的id获取图像结果
|
|
getData('/generateGraph', { method: 'web', result: props.result }).then(response => {
|
|
getData('/generateGraph', { method: 'web', result: props.result }).then(response => {
|
|
- console.log(response.data)
|
|
|
|
nodes.value = []
|
|
nodes.value = []
|
|
edges.value = []
|
|
edges.value = []
|
|
response.data.nodes.forEach(node => {
|
|
response.data.nodes.forEach(node => {
|
|
nodes.value.push(node)
|
|
nodes.value.push(node)
|
|
})
|
|
})
|
|
response.data.edges.forEach(edge => {
|
|
response.data.edges.forEach(edge => {
|
|
- edges.value.push([response.data.nodes.find(n => n.id == edge.from).coordinates, response.data.nodes.find(n => n.id == edge.to).coordinates])
|
|
|
|
|
|
+ edges.value.push([{
|
|
|
|
+ coordinates: response.data.nodes.find(n => n.id == edge.from).coordinates,
|
|
|
|
+ id: edge.from
|
|
|
|
+ }, {
|
|
|
|
+ coordinates: response.data.nodes.find(n => n.id == edge.to).coordinates,
|
|
|
|
+ id: edge.to
|
|
|
|
+ }])
|
|
})
|
|
})
|
|
nextTick(() => {
|
|
nextTick(() => {
|
|
createEdges();
|
|
createEdges();
|
|
@@ -164,6 +460,10 @@ onMounted(() => {
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', handleResize);
|
|
window.removeEventListener('resize', handleResize);
|
|
|
|
+ window.removeEventListener('mousemove', handleHover);
|
|
|
|
+ window.removeEventListener('click', handleClick);
|
|
|
|
+ window.removeEventListener('resize', updateContainerRect)
|
|
|
|
+ window.removeEventListener('scroll', updateContainerRect, true)
|
|
});
|
|
});
|
|
</script>
|
|
</script>
|
|
|
|
|
|
@@ -174,7 +474,10 @@ onBeforeUnmount(() => {
|
|
}
|
|
}
|
|
|
|
|
|
.graph-container {
|
|
.graph-container {
|
|
|
|
+ position: relative;
|
|
height: 100%;
|
|
height: 100%;
|
|
|
|
+ overflow-y: hidden;
|
|
|
|
+ /* 自动显示滚动条 */
|
|
}
|
|
}
|
|
|
|
|
|
canvas {
|
|
canvas {
|
|
@@ -206,4 +509,57 @@ canvas {
|
|
align-items: center;
|
|
align-items: center;
|
|
padding: 0 20px;
|
|
padding: 0 20px;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+.button-group {
|
|
|
|
+ margin-left: auto;
|
|
|
|
+ display: flex;
|
|
|
|
+ gap: 10px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.info-box {
|
|
|
|
+ position: absolute;
|
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
|
+ border: 2px solid #409EFF;
|
|
|
|
+ border-radius: 8px;
|
|
|
|
+ padding: 15px;
|
|
|
|
+ width: 200px;
|
|
|
|
+ min-height: 60px;
|
|
|
|
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
|
|
+ z-index: 1000;
|
|
|
|
+ pointer-events: auto;
|
|
|
|
+ transition: top 0.3s ease;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.info-box[left] {
|
|
|
|
+ left: 20px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.info-box[right] {
|
|
|
|
+ right: 20px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.close-btn {
|
|
|
|
+ position: absolute;
|
|
|
|
+ right: 8px;
|
|
|
|
+ top: 8px;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ font-size: 18px;
|
|
|
|
+ color: #666;
|
|
|
|
+ transition: color 0.2s;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.close-btn:hover {
|
|
|
|
+ color: #409EFF;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.content {
|
|
|
|
+ font-size: 14px;
|
|
|
|
+ color: #303133;
|
|
|
|
+ word-break: break-all;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.connection-line {
|
|
|
|
+ position: absolute;
|
|
|
|
+ pointer-events: none;
|
|
|
|
+}
|
|
</style>
|
|
</style>
|