Browse Source

4-11提交,新增网页视图显示、系统监控

Lan 1 month ago
parent
commit
e9e795ea32

BIN
backend/api/__pycache__/api_user.cpython-310.pyc


+ 2 - 0
backend/api/api_user.py

@@ -30,6 +30,7 @@ class UserRegisterAPI(APIView):
                 "username": newuser.username,
                 "displayname": newuser.displayname,
                 "token": token.key,
+                'identity': newuser.identity,
             },code=201)
         
         # 处理错误信息
@@ -69,6 +70,7 @@ class UserLoginAPI(APIView):
                 'username': user.username,
                 'displayName': user.displayname,
                 'token': token,
+                'identity': user.identity,
             }, code=201)
         else:
             return failed(message="登录失败", data="用户名不存在,或密码错误", code=401)

BIN
backend/db.sqlite3


+ 2 - 0
viewer/package.json

@@ -17,6 +17,8 @@
     "axios": "^1.7.9",
     "echarts": "^5.6.0",
     "element-plus": "^2.9.3",
+    "file-saver": "^2.0.5",
+    "html2canvas": "^1.4.1",
     "sass-embedded": "^1.83.4",
     "three": "^0.175.0",
     "troisjs": "^0.3.4",

+ 47 - 0
viewer/pnpm-lock.yaml

@@ -20,6 +20,12 @@ importers:
       element-plus:
         specifier: ^2.9.3
         version: 2.9.3(vue@3.5.13(typescript@5.7.3))
+      file-saver:
+        specifier: ^2.0.5
+        version: 2.0.5
+      html2canvas:
+        specifier: ^1.4.1
+        version: 1.4.1
       sass-embedded:
         specifier: ^1.83.4
         version: 1.83.4
@@ -843,6 +849,10 @@ packages:
   balanced-match@1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
 
+  base64-arraybuffer@1.0.2:
+    resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==, tarball: https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz}
+    engines: {node: '>= 0.6.0'}
+
   birpc@0.2.19:
     resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==}
 
@@ -910,6 +920,9 @@ packages:
     resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
     engines: {node: '>= 8'}
 
+  css-line-break@2.1.0:
+    resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==, tarball: https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz}
+
   cssesc@3.0.0:
     resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
     engines: {node: '>=4'}
@@ -1096,6 +1109,9 @@ packages:
     resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
     engines: {node: '>=16.0.0'}
 
+  file-saver@2.0.5:
+    resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==, tarball: https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz}
+
   fill-range@7.1.1:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
@@ -1182,6 +1198,10 @@ packages:
     resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
     engines: {node: '>=8'}
 
+  html2canvas@1.4.1:
+    resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==, tarball: https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz}
+    engines: {node: '>=8.0.0'}
+
   human-signals@8.0.0:
     resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==}
     engines: {node: '>=18.18.0'}
@@ -1724,6 +1744,9 @@ packages:
     resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
     engines: {node: ^14.18.0 || >=16.0.0}
 
+  text-segmentation@1.0.3:
+    resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==, tarball: https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz}
+
   three@0.175.0:
     resolution: {integrity: sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==, tarball: https://registry.npmmirror.com/three/-/three-0.175.0.tgz}
 
@@ -1793,6 +1816,9 @@ packages:
   util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
+  utrie@1.0.2:
+    resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==, tarball: https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz}
+
   varint@6.0.0:
     resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==, tarball: https://registry.npmmirror.com/varint/-/varint-6.0.0.tgz}
 
@@ -2709,6 +2735,8 @@ snapshots:
 
   balanced-match@1.0.2: {}
 
+  base64-arraybuffer@1.0.2: {}
+
   birpc@0.2.19: {}
 
   boolbase@1.0.0: {}
@@ -2774,6 +2802,10 @@ snapshots:
       shebang-command: 2.0.0
       which: 2.0.2
 
+  css-line-break@2.1.0:
+    dependencies:
+      utrie: 1.0.2
+
   cssesc@3.0.0: {}
 
   csstype@3.1.3: {}
@@ -3016,6 +3048,8 @@ snapshots:
     dependencies:
       flat-cache: 4.0.1
 
+  file-saver@2.0.5: {}
+
   fill-range@7.1.1:
     dependencies:
       to-regex-range: 5.0.1
@@ -3084,6 +3118,11 @@ snapshots:
 
   html-tags@3.3.1: {}
 
+  html2canvas@1.4.1:
+    dependencies:
+      css-line-break: 2.1.0
+      text-segmentation: 1.0.3
+
   human-signals@8.0.0: {}
 
   ignore@5.3.2: {}
@@ -3523,6 +3562,10 @@ snapshots:
       '@pkgr/core': 0.1.1
       tslib: 2.8.1
 
+  text-segmentation@1.0.3:
+    dependencies:
+      utrie: 1.0.2
+
   three@0.175.0: {}
 
   to-regex-range@5.0.1:
@@ -3577,6 +3620,10 @@ snapshots:
 
   util-deprecate@1.0.2: {}
 
+  utrie@1.0.2:
+    dependencies:
+      base64-arraybuffer: 1.0.2
+
   varint@6.0.0: {}
 
   vite-hot-client@0.2.4(vite@6.0.11(@types/node@22.13.1)(jiti@2.4.2)(sass-embedded@1.83.4)):

+ 4 - 0
viewer/src/router/index.ts

@@ -38,6 +38,10 @@ const router = createRouter({
         {
           path: 'view',
           component: () => import('../views/dashoard/view.vue'),
+        },
+        {
+          path: 'monitor',
+          component: () => import('../views/dashoard/monitor.vue'),
         }
       ]
     }

+ 3 - 0
viewer/src/store/userInfo.js

@@ -4,6 +4,7 @@ export function useUserInfo() {
     const userInfo = ref({
         username: '',
         displayname: '',
+        identity: '',
     });
     const register = async (username, password) => {
         //登录
@@ -23,6 +24,7 @@ export function useUserInfo() {
                         }
                     }
                 )
+                console.log(userInfo.value)
                 registerResult.status = 'success';
                 registerResult.message = '注册成功';
                 registerResult.data = userInfo;
@@ -67,6 +69,7 @@ export function useUserInfo() {
                         }
                     }
                 )
+                console.log(userInfo.value)
                 loginResult.status = 'success';
                 loginResult.message = '登录成功';
                 loginResult.data = userInfo;

+ 78 - 0
viewer/src/views/components/monitor/LineChart.vue

@@ -0,0 +1,78 @@
+<template>
+    <div ref="chart" :style="{ width, height }"></div>
+  </template>
+  
+  <script setup>
+  import { ref, onMounted, watch, onBeforeUnmount } from 'vue'
+  import * as echarts from 'echarts'
+  
+  const props = defineProps({
+    data: Array,
+    width: { type: String, default: '100%' },
+    height: { type: String, default: '300px' }
+  })
+  
+  const chart = ref(null)
+  let chartInstance = null
+  
+  const initChart = () => {
+    chartInstance = echarts.init(chart.value)
+    
+    const option = {
+      grid: { top: 20, right: 30, bottom: 30, left: 40 },
+      xAxis: {
+        type: 'category',
+        data: props.data.map((_, i) => `${i * 10}s`),
+        axisLabel: { color: '#666' }
+      },
+      yAxis: {
+        type: 'value',
+        axisLabel: { 
+          formatter: '{value}%',
+          color: '#666'
+        }
+      },
+      series: [{
+        type: 'line',
+        smooth: true,
+        symbol: 'none',
+        areaStyle: { color: 'rgba(103,194,58,0.1)' },
+        lineStyle: { color: '#67c23a', width: 2 },
+        data: props.data
+      }],
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: { type: 'shadow' }
+      }
+    }
+    
+    chartInstance.setOption(option)
+  }
+  
+  const updateChart = () => {
+    if (!chartInstance) return
+    
+    chartInstance.setOption({
+      xAxis: { data: props.data.map((_, i) => `${i * 10}s`) },
+      series: [{ data: props.data }]
+    })
+  }
+  
+  const handleResize = () => {
+    chartInstance?.resize()
+  }
+  
+  onMounted(() => {
+    initChart()
+    window.addEventListener('resize', handleResize)
+  })
+  
+  onBeforeUnmount(() => {
+    window.removeEventListener('resize', handleResize)
+    chartInstance?.dispose()
+  })
+  
+  watch(() => props.data, () => {
+    updateChart()
+  }, { deep: true })
+  </script>

+ 68 - 0
viewer/src/views/components/monitor/PieChart.vue

@@ -0,0 +1,68 @@
+<template>
+    <div ref="chart" :style="{ height: height }"></div>
+</template>
+
+<script setup>
+import { ref, onMounted, watch, onBeforeUnmount } from 'vue'
+import * as echarts from 'echarts'
+
+const props = defineProps({
+    used: Number,
+    total: Number,
+    height: { type: String, default: '120px' }
+})
+
+const chart = ref(null)
+let chartInstance = null
+const colorMap = {
+    used: '#f56c6c',
+    free: '#e4e7ed'
+}
+
+const initChart = () => {
+    chartInstance = echarts.init(chart.value)
+
+    const option = {
+        tooltip: {
+            formatter: `{a}<br/>{b}: {c}GB ({d}%)`
+        },
+        series: [{
+            name: '磁盘使用',
+            type: 'pie',
+            radius: ['65%', '80%'],
+            avoidLabelOverlap: false,
+            label: {
+                show: false
+            },
+            data: [
+                { value: props.used.toFixed(2), name: '已使用' },
+                { value: (props.total - props.used).toFixed(2), name: '可用' }
+            ],
+            itemStyle: {
+                color: (params) => params.dataIndex === 0 ? colorMap.used : colorMap.free
+            }
+        }]
+    }
+
+    chartInstance.setOption(option)
+}
+
+watch(() => [props.used, props.total], () => {
+    if (chartInstance) {
+        chartInstance.setOption({
+            series: [{
+                data: [
+                    { value: props.used, name: '已使用' },
+                    { value: props.total - props.used, name: '可用' }
+                ],
+                itemStyle: {
+                    color: (params) => params.dataIndex === 0 ? colorMap.used : colorMap.free
+                }
+            }]
+        })
+    }
+})
+
+onMounted(initChart)
+onBeforeUnmount(() => chartInstance?.dispose())
+</script>

+ 93 - 24
viewer/src/views/dashoard/VRView.vue

@@ -1,28 +1,97 @@
 <template>
-    <div>VR
-        {{props.result}}
+    <div class="modal-container">
+      <!-- 加载状态 -->
+      <div v-if="loading" class="loading-container">
+        <el-icon class="loading-icon"><Loading /></el-icon>
+        <span>正在加载验证码...</span>
+      </div>
+  
+      <!-- 验证码显示 -->
+      <div v-else class="code-container">
+        <div class="verification-code">
+          {{ verificationCode }}
+        </div>
+        <p class="code-tip">请使用该验证码在VR设备中查看视图(有效时间5分钟)</p>
+      </div>
     </div>
-</template>
-<script setup>
-import { onMounted, ref } from 'vue'
-import { postData, getData } from '@/api/axios.js'
-import { ElMessage } from 'element-plus'
-const props = defineProps({
+  </template>
+  
+  <script setup>
+  import { ref, onMounted } from 'vue'
+  import { postData, getData } from '@/api/axios.js'
+  import { ElMessage } from 'element-plus'
+  import { Loading } from '@element-plus/icons-vue'
+  
+  const props = defineProps({
     result: {
-        type: Number,
-        required: true,
+      type: Number,
+      required: true,
     }
-})
-// 加载中占位符
-const loading = ref(true)
-
-onMounted( () => {
-    // 根据result的id获取图像结果
-    getData('/generateGraph', {method: 'VR', result: props.result}).then( response => {
-        console.log(response.data)
-    }).catch( error => {
-        ElMessage.error("获取图数据失败")
-        console.log(error)
-    })
-})
-</script>
+  })
+  
+  const loading = ref(true)
+  const verificationCode = ref('') // 存储6位验证码
+  
+  onMounted(() => {
+    getData('/generateGraph', { method: 'VR', result: props.result })
+      .then(response => {
+        // 提取并格式化验证码
+        verificationCode.value = response.data.token.match(/\d{6}/)?.[0] || ''
+        loading.value = false
+      })
+      .catch(error => {
+        ElMessage.error("验证码获取失败")
+        console.error(error)
+        loading.value = false
+      })
+  })
+  </script>
+  
+  <style scoped>
+  .modal-container {
+    min-height: 200px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+  
+  .loading-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 12px;
+  }
+  
+  .loading-icon {
+    font-size: 32px;
+    animation: rotating 2s linear infinite;
+  }
+  
+  @keyframes rotating {
+    from { transform: rotate(0deg); }
+    to { transform: rotate(360deg); }
+  }
+  
+  .code-container {
+    text-align: center;
+  }
+  
+  .verification-code {
+    font-family: 'Courier New', monospace;
+    font-size: 42px;
+    font-weight: 700;
+    letter-spacing: 8px;
+    color: #409EFF;
+    background: #f5f7fa;
+    padding: 20px 40px;
+    border-radius: 8px;
+    display: inline-block;
+    box-shadow: 0 2px 12px rgba(0,0,0,0.1);
+  }
+  
+  .code-tip {
+    color: #909399;
+    margin-top: 16px;
+    font-size: 14px;
+  }
+  </style>

+ 593 - 0
viewer/src/views/dashoard/monitor.vue

@@ -0,0 +1,593 @@
+<template>
+    <!-- monitor.vue -->
+    <div class="monitor-container">
+        <!-- 系统状态概览 -->
+        <div class="system-monitor">
+            <el-row :gutter="16" class="chart-grid">
+                <!-- CPU使用率 -->
+                <el-col :xs="24" :sm="12" :lg="6">
+                    <el-card class="chart-card" shadow="never">
+                        <template #header>
+                            <div class="chart-header">
+                                <div class="title">CPU使用率</div>
+                                <div class="value">{{ `${systemStatus.cpu}%` }}</div>
+                            </div>
+                        </template>
+                        <line-chart :data="cpuHistory" :colors="['#67c23a']" height="120px" />
+                    </el-card>
+                </el-col>
+
+                <!-- 内存占用 -->
+                <el-col :xs="24" :sm="12" :lg="6">
+                    <el-card class="chart-card" shadow="never">
+                        <template #header>
+                            <div class="chart-header">
+                                <div class="title">内存使用</div>
+                                <div class="value">
+                                    {{ `${(systemStatus.memory.used / systemStatus.memory.total * 100).toFixed(1)}%` }}
+                                </div>
+                            </div>
+                        </template>
+                        <line-chart :data="memoryHistory" :colors="['#409eff']" height="120px" />
+                    </el-card>
+                </el-col>
+
+                <!-- 磁盘空间 -->
+                <el-col :xs="24" :sm="12" :lg="6">
+                    <el-card class="chart-card" shadow="never">
+                        <template #header>
+                            <div class="chart-header">
+                                <div class="title">磁盘空间</div>
+                                <div class="value">
+                                    {{ `${(systemStatus.disk.used / systemStatus.disk.total * 100).toFixed(1)}%` }}
+                                </div>
+                            </div>
+                        </template>
+                        <pie-chart :used="systemStatus.disk.used" :total="systemStatus.disk.total" height="120px" />
+                    </el-card>
+                </el-col>
+
+                <!-- 活跃进程 -->
+                <el-col :xs="24" :sm="12" :lg="6">
+                    <el-card class="chart-card" shadow="never">
+                        <template #header>
+                            <div class="chart-header">
+                                <div class="title">活跃进程</div>
+                                <div class="value">{{ activeProcessCount }}</div>
+                            </div>
+                        </template>
+                        <line-chart :data="processHistory" :colors="['#e6a23c']" height="120px" />
+                    </el-card>
+                </el-col>
+            </el-row>
+        </div>
+
+
+        <div class="process-alert-container">
+            <el-row :gutter="20">
+                <!-- 左侧进程列表 -->
+                <el-col :xs="24" :sm="24" :md="16" :lg="18">
+                    <el-card class="process-list">
+
+                        <el-card shadow="never">
+                            <template #header>
+                                <div class="table-header">
+                                    <span>运行中进程({{ processList.length }})</span>
+                                    <el-button type="primary" size="small" @click="refreshProcess">
+                                        <el-icon>
+                                            <Refresh />
+                                        </el-icon>
+                                    </el-button>
+                                </div>
+                            </template>
+
+                            <el-table :data="processList" v-loading="loading">
+                                <el-table-column prop="name" label="进程名称" min-width="150" />
+                                <el-table-column label="CPU占用">
+                                    <template #default="{ row }">
+                                        {{ row.cpu.toFixed(1) }}%
+                                    </template>
+                                </el-table-column>
+                                <el-table-column label="内存占用">
+                                    <template #default="{ row }">
+                                        {{ (row.memory / 1024).toFixed(1) }} MB
+                                    </template>
+                                </el-table-column>
+                                <el-table-column prop="runtime" label="运行时间">
+                                    <template #default="{ row }">
+                                        {{ formatRuntime(row.startTime) }}
+                                    </template>
+                                </el-table-column>
+                                <el-table-column label="磁盘IO">
+                                    <template #default="{ row }">
+                                        ↑{{ row.io.read }} ↓{{ row.io.write }} KB/s
+                                    </template>
+                                </el-table-column>
+                                <el-table-column label="操作" width="150">
+                                    <template #default="{ row }">
+                                        <el-button size="small" @click="pauseProcess(row.pid)">
+                                            暂停
+                                        </el-button>
+                                        <el-button size="small" type="danger" @click="killProcess(row.pid)">
+                                            终止
+                                        </el-button>
+                                    </template>
+                                </el-table-column>
+                            </el-table>
+                        </el-card>
+                    </el-card>
+                </el-col>
+                <el-col :xs="24" :sm="24" :md="8" :lg="6">
+                    <el-card class="alert-config">
+                        <template #header>
+                            <div class="alert-header">
+                                <span>告警规则</span>
+                                <el-button type="primary" size="small" @click="showCreateAlert">
+                                    新建告警
+                                </el-button>
+                            </div>
+                        </template>
+
+                        <!-- 告警规则列表 -->
+                        <el-table :data="alerts" height="400" @row-click="showAlertDetail" style="cursor: pointer;">
+                            <el-table-column prop="name" label="规则名称" />
+                            <el-table-column label="级别">
+                                <template #default="{ row }">{{ row.level === 'system' ? '系统级' : '进程级' }}</template>
+                            </el-table-column>
+                            <el-table-column label="操作" width="80">
+                                <template #default="{ row }">
+                                    <el-button type="danger" size="small" @click="deleteAlert(row.id)">删除</el-button>
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </el-card>
+                </el-col>
+            </el-row>
+        </div>
+        <!-- 告警创建模态框 -->
+        <el-dialog v-model="alertDialogVisible" title="创建告警规则">
+            <el-form :model="newAlert" :rules="alertRules" ref="alertForm">
+                <el-form-item label="规则名称" prop="name">
+                    <el-input v-model="newAlert.name" />
+                </el-form-item>
+
+                <el-form-item label="告警级别" prop="level">
+                    <el-select v-model="newAlert.level">
+                        <el-option label="系统级" value="system" />
+                        <el-option label="进程级" value="process" />
+                    </el-select>
+                </el-form-item>
+
+                <el-form-item label="监控参数" prop="metric">
+                    <el-select v-model="newAlert.metric">
+                        <el-option label="CPU使用率" value="cpu" />
+                        <el-option label="内存使用" value="memory" />
+                        <el-option label="磁盘空间" value="disk" />
+                    </el-select>
+                </el-form-item>
+
+                <el-form-item label="阈值" prop="threshold">
+                    <el-input v-model.number="newAlert.threshold">
+                        <template #append>
+                            <span v-if="newAlert.metric === 'cpu'">%</span>
+                            <span v-else>GB</span>
+                        </template>
+                    </el-input>
+                </el-form-item>
+
+                <el-form-item label="处理方案" prop="action">
+                    <el-select v-model="newAlert.action">
+                        <el-option label="终止高占用进程" value="1" />
+                        <el-option label="终止最新进程" value="2" />
+                        <el-option label="不做处理" value="3" />
+                    </el-select>
+                </el-form-item>
+            </el-form>
+
+            <template #footer>
+                <el-button @click="alertDialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="createAlert">创建</el-button>
+            </template>
+        </el-dialog>
+        <!-- 告警规则详情 -->
+        <el-dialog v-model="detailDialogVisible" title="告警规则详情">
+            <el-descriptions :column="1" border>
+                <el-descriptions-item label="规则名称">{{ selectedAlert?.name }}</el-descriptions-item>
+                <el-descriptions-item label="告警级别">
+                    {{ selectedAlert?.level === 'system' ? '系统级' : '进程级' }}
+                </el-descriptions-item>
+                <el-descriptions-item label="监控参数">
+                    {{ metricMap[selectedAlert?.metric] }}
+                </el-descriptions-item>
+                <el-descriptions-item label="阈值">
+                    {{ selectedAlert?.threshold }}{{ selectedAlert?.metric === 'cpu' ? '%' : 'GB' }}
+                </el-descriptions-item>
+                <el-descriptions-item label="处理方案">
+                    {{ actionMap[selectedAlert?.action] }}
+                </el-descriptions-item>
+                <!-- <el-descriptions-item label="创建时间">
+                    {{ new Date(selectedAlert?.createTime).toLocaleString() }}
+                </el-descriptions-item> -->
+            </el-descriptions>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onBeforeUnmount, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import LineChart from '@/views/components/monitor/LineChart.vue'
+import PieChart from '@/views/components/monitor/PieChart.vue'
+
+// 模拟系统状态数据
+const systemStatus = ref({
+    cpu: 0,
+    memory: { used: 8.2, total: 32 },
+    disk: { used: 287, total: 512 },
+    processes: 0
+})
+
+let updateInterval = null
+const cpuHistory = ref([])
+const memoryHistory = ref([])
+const processHistory = ref([])
+const processList = ref([])
+const loading = ref(false)
+const activeProcessCount = computed(() => systemStatus.value.processes)
+const systemStats = computed(() => [
+    { title: 'CPU', value: `${systemStatus.value.cpu}%` },
+    { title: '内存', value: `${systemStatus.value.memory.used}GB` },
+    { title: '磁盘', value: `${systemStatus.value.disk.used}GB` },
+    { title: '进程', value: systemStatus.value.processes }
+])
+
+// 告警相关数据
+const alerts = ref([])
+const alertDialogVisible = ref(false)
+const newAlert = reactive({
+    name: '',
+    level: 'system',
+    metric: 'cpu',
+    threshold: null,
+    action: '3'
+})
+const detailDialogVisible = ref(false)
+const selectedAlert = ref(null)
+const metricMap = {
+    cpu: 'CPU使用率',
+    memory: '内存使用',
+    disk: '磁盘空间'
+}
+
+const actionMap = {
+    '1': '终止高占用进程',
+    '2': '终止最新进程',
+    '3': '不做任何处理'
+}
+
+
+// 告警规则合法性检测
+const alertRules = {
+    name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
+    level: [{ required: true, message: '请选择告警级别', trigger: 'change' }],
+    metric: [{ required: true, message: '请选择监控参数', trigger: 'change' }],
+    threshold: [
+        { required: true, message: '请输入阈值', trigger: 'blur' },
+        {
+            validator: (_, value, callback) => {
+                if (newAlert.metric === 'cpu') {
+                    return value >= 0 && value <= 100 ? callback() : callback('CPU阈值需在0-100之间')
+                }
+                return value > 0 ? callback() : callback('阈值必须大于0')
+            },
+            trigger: 'blur'
+        }
+    ],
+    action: [{ required: true, message: '请选择处理方案', trigger: 'change' }]
+}
+
+// 显示创建告警规则modal
+const showCreateAlert = () => {
+    alertDialogVisible.value = true
+}
+
+
+// 显示告警规则详情
+const showAlertDetail = (row) => {
+    selectedAlert.value = row
+    detailDialogVisible.value = true
+}
+
+// 创建告警
+const createAlert = () => {
+    //post告警规则到后端
+    alerts.value.push({
+        ...newAlert,
+        id: Date.now()
+    })
+    alertDialogVisible.value = false
+}
+
+// 删除告警
+const deleteAlert = (id) => {
+    alerts.value = alerts.value.filter(a => a.id !== id)
+}
+
+const formatRuntime = (timestamp) => {
+    const seconds = Math.floor((Date.now() - timestamp) / 1000)
+    const h = Math.floor(seconds / 3600)
+    const m = Math.floor((seconds % 3600) / 60)
+    return `${h}h ${m}m`
+}
+
+const fetchSystemStatus = async () => {
+    try {
+        // 实际应调用API接口
+        const mockData = {
+            cpu: +(Math.random() * 100).toFixed(2),
+            memory: { used: +(8 + Math.random() * 4).toFixed(2), total: 32 },
+            disk: { used: +(256 + Math.random() * 10).toFixed(2),  total: 512 },
+            processes: processList.value.length
+        }
+        systemStatus.value = mockData
+        cpuHistory.value = [...cpuHistory.value.slice(-29), systemStatus.value.cpu]
+        memoryHistory.value = [...memoryHistory.value.slice(-29), systemStatus.value.memory.used]
+        processHistory.value = [...processHistory.value.slice(-29), systemStatus.value.processes]
+    } catch (error) {
+        ElMessage.error('获取系统状态失败')
+    }
+}
+
+const fetchProcessList = async () => {
+    loading.value = true
+    try {
+        // 模拟进程数据
+        processList.value = Array.from({ length: 15 }, (_, i) => ({
+            pid: 1000 + i,
+            name: `Process ${i + 1}`,
+            cpu: Math.random() * 100,
+            memory: Math.random() * 1024 * 1024,
+            startTime: Date.now() - Math.random() * 3600000,
+            io: {
+                read: Math.random() * 100,
+                write: Math.random() * 50
+            }
+        }))
+    } finally {
+        loading.value = false
+    }
+}
+
+const processAction = async (pid, action) => {
+    try {
+        const res = await fetch(`/api/process/${pid}`, {
+            method: 'POST',
+            body: JSON.stringify({ action })
+        })
+
+        if (!res.ok) throw new Error()
+        ElMessage.success(`${action === 'pause' ? '暂停' : '终止'}成功`)
+        await fetchProcessList()
+    } catch (error) {
+        ElMessage.error('操作失败,请检查权限或进程状态')
+    }
+}
+
+const pauseProcess = (pid) => processAction(pid, 'pause')
+const killProcess = (pid) => processAction(pid, 'kill')
+
+const refreshProcess = () => {
+    fetchProcessList()
+}
+
+onMounted(() => {
+    fetchSystemStatus()
+    fetchProcessList()
+    updateInterval = setInterval(fetchSystemStatus, 1000)
+})
+
+onBeforeUnmount(() => {
+    clearInterval(updateInterval)
+})
+</script>
+
+<style lang="scss" scoped>
+@media (max-width: 768px) {
+    .system-monitor {
+        height: auto;
+        min-height: unset;
+    }
+
+    .process-list {
+        max-height: none;
+        min-height: unset;
+    }
+
+    .process-alert-container {
+        .el-col {
+            margin-bottom: 16px;
+        }
+    }
+
+    :deep(.el-descriptions) {
+        .el-descriptions__label {
+            width: 90px;
+            font-size: 13px;
+        }
+
+        .el-descriptions__content {
+            font-size: 14px;
+        }
+    }
+}
+
+.monitor-container {
+    display: flex;
+    flex-direction: column;
+    height: 90vh;
+    padding: 16px;
+    background: #f5f7fa;
+    overflow: hidden;
+
+    @media (max-width: 768px) {
+        height: auto;
+        min-height: 100vh;
+    }
+}
+
+.system-monitor {
+    flex: 0 0 auto;
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+    padding: 16px;
+    margin-bottom: 16px;
+}
+
+.compact-stats {
+    margin-bottom: 20px;
+
+    .compact-stat {
+        background: white;
+        padding: 12px;
+        border-radius: 8px;
+        box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+
+        .stat-value {
+            font-size: 18px;
+            font-weight: 500;
+            color: #303133;
+        }
+
+        .stat-title {
+            font-size: 12px;
+            color: #909399;
+        }
+    }
+}
+
+.chart-grid {
+    .el-col {
+        margin-bottom: 16px;
+    }
+}
+
+.process-list {
+    flex: 1;
+    min-height: 300px;
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+    display: flex;
+    flex-direction: column;
+
+    :deep(.el-card__header) {
+        flex-shrink: 0;
+        padding: 12px 16px;
+        background: #fafafa;
+    }
+
+    :deep(.el-card__body) {
+        flex: 1;
+        padding: 0;
+        overflow: hidden;
+        display: flex;
+        flex-direction: column;
+    }
+
+    :deep(.el-table) {
+        flex: 1;
+        overflow: hidden;
+
+        .el-table__body-wrapper {
+            overflow-y: auto !important;
+            max-height: calc(100vh - 180px);
+
+            @media (max-width: 768px) {
+                max-height: none;
+            }
+        }
+    }
+}
+
+.chart-card {
+    margin-bottom: 16px;
+    border-radius: 8px !important;
+    background: white;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
+
+    :deep(.el-card__header) {
+        padding: 12px 16px;
+        border-bottom: none;
+        background: linear-gradient(to right, #f8f9fa, #fff);
+    }
+
+    :deep(.el-card__body) {
+        padding: 12px;
+        height: calc(100% - 56px);
+    }
+
+    .chart-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+
+        .title {
+            font-size: 14px;
+            color: #606266;
+            font-weight: 500;
+        }
+
+        .value {
+            font-size: 16px;
+            color: #303133;
+            font-weight: bold;
+        }
+    }
+}
+
+.process-alert-container {
+    margin-top: 20px;
+}
+
+.alert-config {
+    height: 100%;
+
+    :deep(.el-card__body) {
+        padding: 0;
+    }
+
+    .alert-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+    }
+}
+
+.el-table__row:hover {
+    background-color: #f5f7fa !important;
+}
+
+:deep(.el-descriptions) {
+    .el-descriptions__body {
+        padding: 15px;
+        background: #f8f9fa;
+    }
+
+    .el-descriptions__label {
+        width: 120px;
+        background: #f5f7fa !important;
+        font-weight: 500;
+        color: #606266;
+
+        .is-bordered-label {
+            border-right: 1px solid #ebeef5;
+        }
+    }
+
+    .el-descriptions__content {
+        color: #303133;
+        padding-left: 20px !important;
+    }
+}
+</style>

+ 107 - 36
viewer/src/views/dashoard/select.vue

@@ -1,24 +1,38 @@
 <template>
-  <!-- 默认内容 -->
   <div class="dashboard-main">
-    <div class="button-column left-column">
-      <div class="action-button" @click="navigateTo('analyze')" @mouseenter="hoverLeft = true"
-        @mouseleave="hoverLeft = false">
-        <SvgIcon :name="hoverLeft ? 'analyze-hover' : 'analyze'" />
-        <div class="button-text">我要分析图</div>
+    <div class="main-buttons">
+      <div class="button-column left-column">
+        <div class="action-button" @click="navigateTo('analyze')" @mouseenter="hoverLeft = true"
+          @mouseleave="hoverLeft = false">
+          <SvgIcon :name="hoverLeft ? 'analyze-hover' : 'analyze'" />
+          <div class="button-text">我要分析图</div>
+        </div>
+      </div>
+
+      <div class="button-column right-column">
+        <div class="action-button" @click="navigateTo('view')" @mouseenter="hoverRight = true"
+          @mouseleave="hoverRight = false">
+          <SvgIcon :name="hoverRight ? 'view-hover' : 'view'" />
+          <div class="button-text">我要查看图</div>
+        </div>
       </div>
     </div>
 
-    <div class="button-column right-column">
-      <div class="action-button" @click="navigateTo('view')" @mouseenter="hoverRight = true"
-        @mouseleave="hoverRight = false">
-        <SvgIcon :name="hoverRight ? 'view-hover' : 'view'" />
-        <div class="button-text">我要查看图</div>
+    <div v-if="isAdmin" class="admin-section">
+      <div class="button-column center-column">
+        <div class="action-button" @click="navigateTo('monitor')" @mouseenter="hoverAdmin = true"
+          @mouseleave="hoverAdmin = false">
+          <div class="admin-content">
+            <SvgIcon :name="hoverAdmin ? 'monitor-hover' : 'monitor'" />
+            <div class="button-text">系统监控面板</div>
+          </div>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
+
 <script setup>
 import { ref, computed, inject, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
@@ -28,12 +42,19 @@ const router = useRouter();
 const hoverLeft = ref(false)
 const hoverRight = ref(false)
 
+const useUserInfo = inject('userInfo');
+
+// 检测是否为管理员
+const isAdmin = computed(() => {
+  return useUserInfo.userInfo.value.identity === 'admin'
+})
+
 const navigateTo = (type) => {
   router.push(`/dashboard/${type}`)
 }
 
 onMounted(() => {
-
+  console.log(useUserInfo.userInfo.value)
 })
 </script>
 
@@ -41,42 +62,88 @@ onMounted(() => {
 .dashboard-main {
   height: 100%;
   display: flex;
+  flex-direction: column;
 
-  .button-column {
+  .main-buttons {
     flex: 1;
     display: flex;
-    justify-content: center;
-    align-items: center;
-    transition: all 0.3s;
+    width: 100%;
+    padding: 0 20px 0 20px;
+    box-sizing: border-box;
 
-    .action-button {
-      cursor: pointer;
-      width: min(40vh, 400px);
-      height: min(40vh, 400px);
+    .button-column {
+      flex: 1;
       display: flex;
-      flex-direction: column;
       justify-content: center;
       align-items: center;
-      background: #fff;
-      border-radius: 16px;
-      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
       transition: all 0.3s;
 
-      &:hover {
-        transform: translateY(-5px);
-        box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
-      }
+      .action-button {
+        cursor: pointer;
+        width: min(40vh, 400px);
+        height: min(40vh, 400px);
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        background: #fff;
+        border-radius: 16px;
+        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+        transition: all 0.3s;
 
-      .button-text {
-        margin-top: 24px;
-        font-size: 1.5em;
-        color: #606266;
-        font-weight: 500;
+        &:hover {
+          transform: translateY(-5px);
+          box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);
+        }
+
+        .button-text {
+          margin-top: 24px;
+          font-size: 1.5em;
+          color: #606266;
+          font-weight: 500;
+        }
+
+        svg {
+          width: 50%;
+          height: 50%;
+        }
       }
+    }
+  }
+
+  .admin-section {
+    flex-shrink: 0;
+    height: 25vh;
+    display: flex;
+    padding: 0 20px 0 20px;
+    justify-content: center;
+
+    .button-column {
+      flex: 1;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      transition: all 0.3s;
 
-      svg {
-        width: 50%;
-        height: 50%;
+      .action-button {
+        width: 60%;
+        height: min(20vh, 200px);
+        border-radius: 24px;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        background: #fff;
+        border-radius: 16px;
+        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+        transition: all 0.3s;
+
+        .button-text {
+          margin-top: 24px;
+          font-size: 1.5em;
+          color: #606266;
+          font-weight: 500;
+        }
       }
     }
   }
@@ -88,5 +155,9 @@ onMounted(() => {
   .right-column {
     background: linear-gradient(135deg, #f5f7fa 0%, #fff7e6 100%);
   }
+
+  .center-column {
+    background: linear-gradient(135deg, #f5f7fa 0%, #ffe6e6 100%);
+  }
 }
 </style>

+ 400 - 44
viewer/src/views/dashoard/threeDView.vue

@@ -3,10 +3,12 @@
         <el-card class="full-width-card">
             <div class="toolbar">
                 <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 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" />
                     <Scene ref="scene">
                         <!-- 使用空组件的 ref 绑定场景 -->
@@ -15,85 +17,336 @@
                 </Renderer>
             </div>
         </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>
 </template>
 <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 { ElMessage } from 'element-plus'
 import { Renderer, Camera, Scene, Group } from 'troisjs';
 import { MeshBasicMaterial } from 'three';
 import * as THREE from 'three';
+import html2canvas from 'html2canvas';
+
 const props = defineProps({
     result: {
         type: Number,
         required: true,
     }
 })
-// 加载中占位符
+// 状态管理
 const loading = ref(true)
 const nodes = ref([])
 const edges = ref([])
 const lines = ref([])
 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 sceneBoundingSphere = new THREE.Sphere()
+
+// 相机初始状态
 const initialCameraState = ref({
     position: 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 = () => {
-    // 创建节点
     // 清理旧对象
     if (nodesGroup.value?.group) {
         nodesGroup.value.group.children = [];
     }
 
-    // 创建蓝色节点
+    // 创建节点
     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 => {
-        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 = () => {
     nextTick(() => {
         if (renderer.value?.renderer) {
@@ -107,7 +360,7 @@ const handleResize = () => {
 };
 
 
-// 修正重置函数
+// 重置视角函数
 const resetCamera = () => {
     console.log(initialCameraState.value.position)
 
@@ -121,18 +374,61 @@ const resetCamera = () => {
     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(() => {
     window.addEventListener('resize', handleResize);
+    window.addEventListener('mousemove', handleHover);
+    window.addEventListener('click', handleClick);
+    // 用于监控视图容器变化
+    window.addEventListener('resize', updateContainerRect)
+    window.addEventListener('scroll', updateContainerRect, true)
     // 根据result的id获取图像结果
     getData('/generateGraph', { method: 'web', result: props.result }).then(response => {
-        console.log(response.data)
         nodes.value = []
         edges.value = []
         response.data.nodes.forEach(node => {
             nodes.value.push(node)
         })
         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(() => {
             createEdges();
@@ -164,6 +460,10 @@ onMounted(() => {
 
 onBeforeUnmount(() => {
     window.removeEventListener('resize', handleResize);
+    window.removeEventListener('mousemove', handleHover);
+    window.removeEventListener('click', handleClick);
+    window.removeEventListener('resize', updateContainerRect)
+    window.removeEventListener('scroll', updateContainerRect, true)
 });
 </script>
 
@@ -174,7 +474,10 @@ onBeforeUnmount(() => {
 }
 
 .graph-container {
+    position: relative;
     height: 100%;
+    overflow-y: hidden;
+    /* 自动显示滚动条 */
 }
 
 canvas {
@@ -206,4 +509,57 @@ canvas {
     align-items: center;
     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>