Lan před 1 měsícem
rodič
revize
380773815d

binární
backend/api/__pycache__/api_calculate.cpython-310.pyc


binární
backend/api/__pycache__/api_graph.cpython-310.pyc


binární
backend/api/__pycache__/api_prepare.cpython-310.pyc


+ 2 - 2
backend/api/api_calculate.py

@@ -38,7 +38,7 @@ class CalculateAPI(APIView):
     # 进行状态检查
     if command == 'start':
       # 如任务已经启动,则不操作
-      if not mission.state in ['init', 'pause']:
+      if not mission.state in ['init', 'pause', 'stop']:
         return success(message="任务正在进行中")
       if mission.state in ['done']:
         return success(message="任务已完成")
@@ -193,7 +193,7 @@ class CalculateAPI(APIView):
       response = requests.post(SCHEDULER_BASE_URL + '/stopMission', json=calculateData)
       if response.json()['code'] == 'OK':
         # 停止后所有任务相关数据删除,恢复初始状态
-        mission.state = 'init'
+        mission.state = 'stop'
         mission.save()
         # 停止后,所有plan的进度需要全部归零,即删除所有result,或将result的进度改为-2
         # 尝试删除所有result方案,下次运行时会重新创建result

+ 7 - 16
backend/api/api_graph.py

@@ -10,7 +10,7 @@ from django.middleware.csrf import get_token
 from django.contrib.auth import login
 
 from api.utils import *
-from api.models import Result, Graph, GraphToken
+from api.models import Result, Graph, GraphToken, Plan
 from random import randint
 
 import json, csv
@@ -41,22 +41,13 @@ class GenerateGraph(APIView):
     def get(self, request):
         user = request.user
         method = request.GET.get('method') # 以何种视图查看
-        resultId = request.GET.get('result')
-        # 测试用,固定result值,省略计算
-        resultId = 57
-        # 检查result的传递值是否正确
+        planId = request.GET.get('plan')
         try:
-            if type(resultId) == int:
-                result = Result.objects.get(id=resultId)
-            elif type(resultId) == str:
-                result = Result.objects.get(id=int(resultId))
-            elif type(resultId) == dict:
-                if 'id' in resultId:
-                    result = Result.objects.get(id=resultId['id'])
-                else:
-                    return failed(message="输入信息错误")
-        except Exception as error:
-            return failed(message="输入信息错误", data=error)
+            plan = Plan.objects.get(id=planId)
+        except Plan.DoesNotExist:
+            print("获取结果的Plan失败")
+            return failed(message="无法找到该结果对应规划")
+        result = plan.own_result
 
         # 图表显示不生成图,仅做统计计算
         if method == 'chart':

+ 27 - 0
backend/api/api_prepare.py

@@ -211,6 +211,33 @@ class InputFileAPI(APIView):
 
 
 class PlanAPI(APIView):
+    def get(self, request):
+        user = request.user
+        try:
+            mission = Mission.objects.get(id=request.GET.get('mission'))
+        except Mission.DoesNotExist:
+            print("处理规划所属任务不存在", request.GET.get('mission'))
+            return failed(message="未找到规划任务所属任务")
+        plans = mission.own_plans.all()
+        response = []
+        for plan in plans:
+            if plan.parent:
+                response.append({
+                    'id': plan.id,
+                    'parent': plan.parent.id,
+                    # 使用名称检索算法
+                    'algorithm': plan.algorithm.name,
+                })
+            else:
+                response.append({
+                    'id': plan.id,
+                    'parent': None,
+                    # 使用名称检索算法
+                    'algorithm': None,
+                })
+        return success(data=response)
+
+
     def post(self, request):
         user = request.user
         plans = request.data.get('plans')

binární
backend/api/models/__pycache__/file.cpython-310.pyc


binární
backend/api/models/__pycache__/mission.cpython-310.pyc


+ 16 - 15
backend/api/models/file.py

@@ -145,15 +145,15 @@ class File(models.Model):
                 csvFile = csv.writer(file)
                 for line in data:
                     if not str(line[0]).isdigit():
-                        print("check file illegal failed", "node", "id wrong")
+                        logger.error("check file illegal failed node id wrong")
                         return FAILED
                     if not line[1] in ['S', 'D', 'I']:
-                        print("check file illegal failed", "node", "type wrong")
+                        logger.error("check file illegal failed node type wrong")
                         return FAILED
                     if line[0] not in nodes:
                         nodes.append(line[0])
                     else:
-                        print("check file illegal failed", "node", "dudplicate id")
+                        logger.error("check file illegal failed node dudplicate id")
                         return FAILED
                     # 除了节点编号和节点类型外,其余参数全部放在line的后续位置,以字符串json的格式保存
                     csvFile.writerow(line)
@@ -165,7 +165,7 @@ class File(models.Model):
                 csvFile = csv.writer(file)
                 for line in data:
                     if not str(line[0]).isdigit() or not str(line[1]).isdigit():
-                        print("check file illegal failed", "edge", "len =2")
+                        logger.error("check file illegal failed edge len =2")
                         return FAILED
                     # 注意默认将边视为无向边
                     # 检查重复
@@ -197,7 +197,7 @@ class File(models.Model):
                 f.close()
                 return OK
         except Exception as error:
-            print(error)
+            logger.error(error)
             return FAILED
     
     # 检查文件是否合法
@@ -211,18 +211,18 @@ class File(models.Model):
                 nodes = []
                 for line in file:
                     if not len(line) >= 2:
-                        print("check file illegal failed", "node", "len >= 2")
+                        logger.error("check file illegal failed node len >= 2")
                         return False
                     if not line[0].isdigit():
-                        print("check file illegal failed", "node", "id wrong")
+                        logger.error("check file illegal failed node id wrong")
                         return False
                     if not line[1] in ['S', 'D', 'I']:
-                        print("check file illegal failed", "node", "type wrong")
+                        logger.error("check file illegal failed node type wrong")
                         return False
                     if line[0] not in nodes:
                         nodes.append(line[0])
                     else:
-                        print("check file illegal failed", "node", "dudplicate id")
+                        logger.error("check file illegal failed node dudplicate id")
                         return False
                 return True
         if self.content == 'edge':
@@ -234,24 +234,25 @@ class File(models.Model):
                 edges = []
                 for line in nodeFile:
                     if not len(line) >= 2:
-                        print("check file illegal failed", "node", "len >= 2")
+                        logger.error("check file illegal failed node len >= 2")
                         return False
                     if not line[0].isdigit():
-                        print("check file illegal failed", "node", "id wrong")
+                        logger.error("check file illegal failed node id wrong")
                         return False
                     nodes.append(line[0])
                 for line in edgeFile:
                     if not len(line) == 2:
-                        print("check file illegal failed", "edge", "len =2")
+                        logger.error("check file illegal failed edge len =2")
                         return False
                     if line[0] not in nodes or line[1] not in nodes:
-                        print("check file illegal failed", "edge", "id not exist")
+                        logger.error("check file illegal failed edge id not exist")
                         return False
                     if [line[0], line[1]] not in edges and [line[1], line[0]] not in edges:
                         edges.append([line[0], line[1]])
                     else:
                         # 将图视为无向图,同一条边的正反算作重复
-                        print("check file illegal failed", "edge", "duplicate edge")
+                        # 直接去除重复边
+                        logger.error("check file illegal failed edge duplicate edge")
                         return False
                 return True
     
@@ -309,7 +310,7 @@ class File(models.Model):
                     os.remove(p)
                 except Exception as error:
                     # 可能出现失败的原因是文件被占用
-                    print("删除文件" + self.id + self.name + "失败", error)
+                    logger.error(f"删除文件{self.id} {self.name}失败:{error}")
                     failedFlag = True
         # 无论文件删除是否成功,都要把记录删除,多余的文件可以再后续清理时删除
         if self.associate:

+ 1 - 0
backend/api/models/mission.py

@@ -6,6 +6,7 @@ state = [
     ('init', 'init'),
     ('calculating', 'calculating'),
     ('pause', 'pause'),
+    ('stop', 'stop'),
     ('done', 'done'),
 ]
 

binární
backend/backend/__pycache__/settings.cpython-310.pyc


+ 10 - 2
backend/backend/settings.py

@@ -25,7 +25,7 @@ SECRET_KEY = 'django-insecure-1wvwk*vqn21we=)usoe)+w13!+4-sq3_g@0-&j-wa0#lq6yf1=
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
 
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['localhost', '127.0.0.1', '8.149.247.53']
 
 
 # Application definition
@@ -62,10 +62,18 @@ MIDDLEWARE = [
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
 ]
 
-CORS_ORIGIN_ALLOW_ALL = True
+
+CORS_ORIGIN_ALLOW_ALL = False
 CORS_ALLOW_METHODS=("*")
 CORS_ALLOW_CREDENTIALS = True
 
+# 放置生产环境前端host
+CORS_ALLOWED_ORIGINS = []
+
+if DEBUG:
+    CORS_ORIGIN_ALLOW_ALL = True
+
+
 AUTH_USER_MODEL = "api.User"
 
 

binární
backend/db.sqlite3


+ 2 - 2
viewer/src/App.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
 import { provide } from 'vue'
 import { RouterLink, RouterView } from 'vue-router'
-import { useUserInfo } from './store/userInfo.js'
-import { useAnalyzeInfo } from './store/analyzeInfo.js'
+import { useUserInfo } from '@/store/userInfo.js'
+import { useAnalyzeInfo } from '@/store/analyzeInfo.js'
 
 provide('userInfo', useUserInfo());
 provide('analyzeInfo', useAnalyzeInfo())

+ 2 - 1
viewer/src/api/axios.js

@@ -1,6 +1,7 @@
 import axios from 'axios';
 
-const baseURL = "http://localhost:8000/api";
+// const baseURL = "http://localhost:8000/api";
+const baseURL = "http://8.149.247.53:9174/api";
 
 let csrfToken = "";
 let userToken = "";

+ 14 - 14
viewer/src/router/index.ts

@@ -6,30 +6,30 @@ const router = createRouter({
     {
       path: '/',
       name: 'login',
-      component: () => import('../views/login/login_container.vue'),
+      component: () => import('@/views/login/login_container.vue'),
     },
     {
       path: '/dashboard',
       name: 'dashboard',
-      component: () => import('../views/dashoard/dashboard.vue'),
+      component: () => import('@/views/dashoard/dashboard.vue'),
       redirect: '',
       children: [
         {
           path: '',
           name: 'select',
-          component: () => import('../views/dashoard/select.vue'),
+          component: () => import('@/views/dashoard/select.vue'),
         },
         {
           path: 'analyze',
-          component: () => import('../views/dashoard/analyze.vue'),
+          component: () => import('@/views/dashoard/analyze.vue'),
           children: [
             {
               path: 'plan',
-              component: () => import('../views/dashoard/plan.vue'),
+              component: () => import('@/views/dashoard/plan.vue'),
               children: [
                 {
                   path: 'calculate',
-                  component: () => import('../views/dashoard/calculate.vue'),
+                  component: () => import('@/views/dashoard/calculate.vue'),
                 }
               ]
             }
@@ -37,15 +37,15 @@ const router = createRouter({
         },
         {
           path: 'view',
-          component: () => import('../views/dashoard/view.vue'),
+          component: () => import('@/views/dashoard/view.vue'),
           children: [
             {
               path: 'plan',
-              component: () => import('../views/dashoard/plan.vue'),
+              component: () => import('@/views/dashoard/plan.vue'),
               children: [
                 {
                   path: 'calculate',
-                  component: () => import('../views/dashoard/calculate.vue'),
+                  component: () => import('@/views/dashoard/calculate.vue'),
                 }
               ]
             }
@@ -53,25 +53,25 @@ const router = createRouter({
         },
         {
           path: 'monitor',
-          component: () => import('../views/dashoard/monitor.vue'),
+          component: () => import('@/views/dashoard/monitor.vue'),
         },
         {
           path:'profile',
-          component: () => import('../views/dashoard/profile.vue'),
+          component: () => import('@/views/dashoard/profile.vue'),
         },
         {
           path:'graphicfile',
-          component: () => import('../views/dashoard/graphicfile.vue'),
+          component: () => import('@/views/dashoard/graphicfile.vue'),
         },
         {
           path:'taskfile',
           // 修改为已有的missionsviewer
-          component: () => import('../views/dashoard/view.vue'),
+          component: () => import('@/views/dashoard/view.vue'),
         },
         {
           path:'networkfile',
           // 修改为已有的missionsviewer
-          component: () => import('../views/dashoard/networkfile.vue'),
+          component: () => import('@/views/dashoard/networkfile.vue'),
         }
       ]
     }

+ 2 - 2
viewer/src/store/analyzeInfo.js

@@ -4,14 +4,14 @@ export function useAnalyzeInfo() {
     const analyzeInfo = ref({
         nodeFile: {
             id: null, 
-            num: 0,
+            amount: 0,
             sNodes: 0,
             dNodes: 0,
             iNodes: 0,
         },
         edgeFile: {
             id: null,
-            num: 0,
+            amount: 0,
         },
         mission: {
             id: null,

+ 4 - 1
viewer/src/types.d.ts

@@ -8,4 +8,7 @@ declare module "@/views/dashoard/analyze.vue"
 declare module "@/views/dashoard/plan.vue"
 declare module "@/views/dashoard/calculate.vue"
 declare module "@/views/dashoard/view.vue"
-declare module "@/views/dashoard/monitor.vue"
+declare module "@/views/dashoard/monitor.vue"
+declare module "@/views/dashoard/profile.vue"
+declare module "@/views/dashoard/graphicfile.vue"
+declare module "@/views/dashoard/networkfile.vue"

+ 2 - 2
viewer/src/views/dashoard/VRView.vue

@@ -23,7 +23,7 @@
   import { Loading } from '@element-plus/icons-vue'
   
   const props = defineProps({
-    result: {
+    plan: {
       type: Number,
       required: true,
     }
@@ -33,7 +33,7 @@
   const verificationCode = ref('') // 存储6位验证码
   
   onMounted(() => {
-    getData('/generateGraph', { method: 'VR', result: props.result })
+    getData('/generateGraph', { method: 'VR', plan: props.plan })
       .then(response => {
         // 提取并格式化验证码
         verificationCode.value = response.data.token.match(/\d{6}/)?.[0] || ''

+ 36 - 13
viewer/src/views/dashoard/analyze.vue

@@ -135,7 +135,7 @@
       </el-col>
 
       <!-- 右侧说明 -->
-      <el-col :span="8">
+      <el-col  v-if="inputMethod === 'input' || inputMethod === 'upload'" :span="8">
         <el-card class="instruction-section">
           <h3>文件格式说明</h3>
           <div class="instruction-content">
@@ -143,11 +143,13 @@
               <el-tag type="success" class="format-tag">节点文件格式</el-tag>
               <el-divider />
               <div class="format-example">
-                <p>节点编号 节点类型 节点描述 节点名称</p>
+                <P>文件应使用CSV格式</P>
+                <p>每一行按照:“节点编号,节点类型,节点名称” 顺序放置数据</p>
                 <p class="example-text">示例:</p>
-                <pre>001 User "普通用户" 张三
-            002 Product "电子产品" 手机
-            003 Category "商品类目" 数码</pre>
+                <pre>
+    1,S,侦察节点
+    2,D,决策节点
+    3,I,打击节点</pre>
               </div>
             </div>
 
@@ -155,11 +157,13 @@
               <el-tag type="warning" class="format-tag">边文件格式</el-tag>
               <el-divider />
               <div class="format-example">
-                <p>起始节点 终止节点</p>
+                <P>文件应使用CSV格式</P>
+                <p>每一行按照:“起始节点,终止节点” 顺序放置数据</p>
                 <p class="example-text">示例:</p>
-                <pre>001 002
-            002 003
-            003 001</pre>
+                <pre>
+    1,2
+    2,3
+    1,3</pre>
               </div>
             </div>
           </div>
@@ -170,8 +174,8 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, inject } from 'vue'
-import { useRouter } from 'vue-router'
+import { ref, computed, onMounted, inject, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Upload, ArrowDown, Plus } from '@element-plus/icons-vue'
 import { getData, postData, deleteData, postFile } from '@/api/axios.js'
@@ -419,14 +423,14 @@ const preparePlan = (response) => {
   response.data.forEach(file => {
     if (file.content === 'node') {
       useAnalyzeInfo.analyzeInfo.value.nodeFile.id = file.id
-      useAnalyzeInfo.analyzeInfo.value.nodeFile.num = file.ndoes
+      useAnalyzeInfo.analyzeInfo.value.nodeFile.amount = file.ndoes
       useAnalyzeInfo.analyzeInfo.value.nodeFile.sNodes = file.sNodes
       useAnalyzeInfo.analyzeInfo.value.nodeFile.dNodes = file.dNodes
       useAnalyzeInfo.analyzeInfo.value.nodeFile.iNodes = file.iNodes
     }
     if (file.content === 'edge') {
       useAnalyzeInfo.analyzeInfo.value.edgeFile.id = file.id
-      useAnalyzeInfo.analyzeInfo.value.edgeFile.num = file.edges
+      useAnalyzeInfo.analyzeInfo.value.edgeFile.amount = file.edges
     }
     // 获取创建的分析任务ID
     if (file.content === 'mission') {
@@ -434,6 +438,8 @@ const preparePlan = (response) => {
       useAnalyzeInfo.analyzeInfo.value.mission.name = file.name
     }
   })
+  // 将获取到的数据写入页面缓存,防止刷新丢失
+  sessionStorage.setItem('analyze-info', JSON.stringify(useAnalyzeInfo.analyzeInfo.value))
   // 跳转到规划页面
   inputMethod.value = "done";
   console.log(useAnalyzeInfo.analyzeInfo.value)
@@ -442,6 +448,23 @@ const preparePlan = (response) => {
   updateUploadHistory()
 }
 
+const route = useRoute();
+// 监听路由变化
+watch(
+  () => route.path,
+  (newPath) => {
+    // 路由变为plan时,修改显示内容
+    if (newPath === '/dashboard/analyze/plan' || newPath === '/dashboard/analyze/plan/calculate') {
+      inputMethod.value = "done"
+    }
+    if (newPath === '/dashboard/analyze'){
+      inputMethod.value = "upload"
+    }
+  },
+  { immediate: true }
+);
+
+
 </script>
 
 <style lang="scss" scoped>

+ 86 - 6
viewer/src/views/dashoard/calculate.vue

@@ -63,7 +63,7 @@
       <!-- 动态内容区域 -->
       <div class="content-area">
         <template v-if="currentView">
-          <component :is="currentViewComponent" :result="currentResult" />
+          <component :is="currentViewComponent" :plan="currentResult" />
         </template>
         <el-empty v-else description="请选择视图展示方式" class="empty-tip">
           <template #image>
@@ -84,9 +84,10 @@
   </div>
 </template>
 <script setup>
-import { inject, onMounted, onUnmounted, ref, computed, defineAsyncComponent, shallowRef } from 'vue'
+import { inject, onMounted, onUnmounted, ref, computed, defineAsyncComponent, shallowRef, watch, nextTick } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { postData, getData } from '@/api/axios.js'
+import { useRoute } from 'vue-router'
 import {
   PieChart,
   Promotion,
@@ -104,6 +105,12 @@ const flowContainer = ref(null)
 const buttonRefs = ref(null)
 const progressInterval = ref(null)
 
+// 布局参数
+const COLUMN_GAP = 300
+const ROW_GAP = 100
+const MAX_COLUMNS = 3
+const MAX_ROWS = 5
+
 // 视图展示控制变量
 // 加载组件
 const ChartView = defineAsyncComponent(() => import('./chartView.vue'))
@@ -164,10 +171,8 @@ const viewResult = (index) => {
     // 初始化视图显示框
     currentView.value = null
     currentViewComponent.value = null
-    currentResult.value = null
-    currentResult.value = index
+    currentResult.value = plans.value[index].id
     showModal.value = true
-
   } else {
     ElMessage.warning("请先开始计算处理,或等待该任务完成")
   }
@@ -211,7 +216,63 @@ const getButtonCenter = (plan) => {
   }
 }
 
+// 自动布局计算
+const calculateLayout = () => {
+  nextTick(() => {
+    const container = flowContainer.value
+    if (!container) return
+
+    const containerWidth = container.offsetWidth
+    const layoutMap = new Map() // 使用Map记录行列位置
+
+    // 第一代按钮布局
+    let currentRow = 0
+    let currentCol = 0
+
+    plans.value.forEach((btn) => {
+      // 仅处理根节点按钮
+      if (!btn.parentId) {
+        btn.row = currentRow
+        btn.col = currentCol
+        currentCol++
+        if (currentCol >= MAX_COLUMNS) {
+          currentCol = 0
+          currentRow++
+        }
+      }
+    })
+
+    // 子节点布局
+    plans.value.forEach((btn) => {
+      if (btn.parentId) {
+        const parent = plans.value.find(b => b.id === btn.parentId)
+        if (!parent) return
+
+        // 处理换行
+        if (btn.col >= MAX_COLUMNS) {
+          btn.row += Math.floor(btn.col / MAX_COLUMNS)
+          btn.col = btn.col % MAX_COLUMNS
+        }
+      }
+
+      // 计算绝对位置(无响应式更新)
+      const pos = {
+        x: 20 + btn.col * COLUMN_GAP,
+        y: btn.row * ROW_GAP
+      }
 
+      // 直接赋值避免触发响应式更新
+      if (btn.position.x !== pos.x || btn.position.y !== pos.y) {
+        btn.position.x = pos.x
+        btn.position.y = pos.y
+      }
+    })
+    setTimeout(() => {
+      updateConnections()
+    }, 200)
+
+  })
+}
 
 // 更新连接线
 const updateConnections = () => {
@@ -345,7 +406,10 @@ const stopCalculate = async () => {
 }
 
 onMounted(() => {
-  setTimeout(() => { updateConnections() }, 500)
+  setTimeout(() => {
+    calculateLayout()
+    updateConnections()
+  }, 500)
 })
 
 onUnmounted(() => {
@@ -356,6 +420,22 @@ onUnmounted(() => {
   }
 })
 
+const route = useRoute();
+// 监听路由变化
+watch(
+  () => route.path,
+  (newPath) => {
+    // 路由变为plan时,修改显示内容
+    if (newPath === '/dashboard/analyze/plan/calculate') {
+      setTimeout(() => {
+        calculateLayout()
+        updateConnections()
+      }, 500)
+    }
+  },
+  { immediate: true }
+);
+
 </script>
 <style lang="scss" scoped>
 .container {

+ 2 - 2
viewer/src/views/dashoard/chartView.vue

@@ -48,7 +48,7 @@ import { ElLoading } from 'element-plus'
 import { getData } from '@/api/axios.js'
 
 const props = defineProps({
-    result: {
+    plan: {
         type: Number,
         required: true,
     }
@@ -84,7 +84,7 @@ const colorPalette = [
 const fetchData = async () => {
     try {
         // 这里替换为实际的API调用
-        const response = await getData('/generateGraph', {method: 'chart', result: props.result})
+        const response = await getData('/generateGraph', {method: 'chart', plan: props.plan})
         data.value = response.data
     } finally {
         loading.value = false

+ 1 - 2
viewer/src/views/dashoard/dashboard.vue

@@ -17,8 +17,7 @@
             </span>
             <template #dropdown>
               <el-dropdown-menu>
-                <el-dropdown-item @click="handleMenu('message')">我的消息</el-dropdown-item>
-                <el-dropdown-item @click="handleMenu('graphicfile')">我的图形数据</el-dropdown-item>
+                <!-- <el-dropdown-item @click="handleMenu('graphicfile')">我的图形数据</el-dropdown-item> -->
                 <el-dropdown-item @click="handleMenu('taskfile')">我的分析任务</el-dropdown-item>
                 <el-dropdown-item @click="handleMenu('networkfile')">我的上传文件</el-dropdown-item>
                 <el-dropdown-item @click="handleMenu('profile')">个人中心</el-dropdown-item>

+ 189 - 60
viewer/src/views/dashoard/plan.vue

@@ -16,13 +16,13 @@
           <el-form-item>
             <div class="stats-item">
               <el-tooltip content="聚焦节点说明" placement="top">
-                <span>聚焦节点数: {{ nodeData.num }}</span>
+                <span>聚焦节点数: {{ nodeData.amount }}</span>
               </el-tooltip>
               <el-tooltip content="孤立节点说明" placement="top">
-                <span>孤立节点数: {{ nodeData.num }}</span>
+                <span>孤立节点数: {{ nodeData.amount }}</span>
               </el-tooltip>
               <el-tooltip content="总节点说明" placement="top">
-                <span>总节点数: {{ nodeData.num }}</span>
+                <span>总节点数: {{ nodeData.amount }}</span>
               </el-tooltip>
             </div>
           </el-form-item>
@@ -32,7 +32,7 @@
               <el-tooltip content="边集中度说明" placement="top">
                 <span>边集中度: {{ edgeData.concentration }}</span>
               </el-tooltip>
-              <span>总边数: {{ edgeData.num }}</span>
+              <span>总边数: {{ edgeData.amount }}</span>
             </div>
           </el-form-item>
         </el-form>
@@ -111,9 +111,9 @@
 </template>
 
 <script setup>
-import { ref, reactive, onMounted, nextTick, inject, provide } from 'vue'
+import { ref, reactive, onMounted, nextTick, inject, provide, watch } from 'vue'
 import { ElMessage } from 'element-plus'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import { Delete, ArrowDown, ArrowRight, VideoPlay, VideoPause, SwitchButton } from '@element-plus/icons-vue'
 import { getData, postData, deleteData } from '@/api/axios.js'
 import { pl } from 'element-plus/es/locales.mjs'
@@ -122,9 +122,9 @@ import { pl } from 'element-plus/es/locales.mjs'
 const useAnalyzeInfo = inject('analyzeInfo')
 const router = useRouter()
 
-const nodeData = useAnalyzeInfo.analyzeInfo.value.nodeFile
-const edgeData = useAnalyzeInfo.analyzeInfo.value.edgeFile
-const mission = useAnalyzeInfo.analyzeInfo.value.mission
+const nodeData = ref(useAnalyzeInfo.analyzeInfo.value.nodeFile)
+const edgeData = ref(useAnalyzeInfo.analyzeInfo.value.edgeFile)
+const mission = ref(useAnalyzeInfo.analyzeInfo.value.mission)
 // 算法列表
 const algorithms = ref([
   { label: '算法A', value: 'algA', level: 0 },
@@ -178,38 +178,46 @@ const MAX_COLUMNS = 3
 const MAX_ROWS = 5
 
 
-// 获取初始数据
-onMounted(async () => {
-  try {
-    // 这里替换实际API调用
-    await fetchData()
-  } finally {
-    loading.value = false
-  }
-})
+
 
 const fetchData = async () => {
+  // 如果当前没有保存的节点和边的数据,则尝试获取任务数据
   // 如果已经保存了节点和边的数据,则省略获取数据
-  if (nodeData.id && edgeData.id) {
-    console.log("跳过获取数据")
+  // 更新节点、边、任务数据
+  console.log(useAnalyzeInfo.analyzeInfo.value)
+  if (nodeData.value.amound == 0 || edgeData.value.amound == 0 || mission.value.id == null) {
+    const recoveryData = JSON.parse(sessionStorage.getItem('analyze-info'))
+    if (recoveryData == null) {
+      ElMessage.error("未获取到当前任务信息")
+      router.push("/dashboard")
+    } else {
+      useAnalyzeInfo.analyzeInfo.value = recoveryData
+    }
+  }
+  nodeData.value = useAnalyzeInfo.analyzeInfo.value.nodeFile
+  edgeData.value = useAnalyzeInfo.analyzeInfo.value.edgeFile
+  mission.value = useAnalyzeInfo.analyzeInfo.value.mission
+  console.log(nodeData)
+  if (nodeData.value.amound == 0 || edgeData.value.amound == 0 || mission.value.id == null) {
+    console.log("从缓存中获取节点、边、任务数据失败")
+    ElMessage.error("从缓存中获取节点、边、任务数据失败")
     return
   }
-  // 如果是从历史文件进入的规划,则应当获取节点和边的数据
-  loading.value = true
-  // 模拟API请求
-  await new Promise(resolve => setTimeout(resolve, 1000))
-  Object.assign(nodeData, {
-    sNodes: 10,
-    dNodes: 5,
-    i: 8,
-    focus: 15,
-    isolate: 3,
-    total: 23
-  })
-  Object.assign(edgeData, {
-    concentration: 0.75,
-    total: 45
-  })
+  // 当前进入plan必须先选择mission,所以不必再获取信息
+  // loading.value = true
+  // await new Promise(resolve => setTimeout(resolve, 1000))
+  // Object.assign(nodeData, {
+  //   sNodes: 10,
+  //   dNodes: 5,
+  //   i: 8,
+  //   focus: 15,
+  //   isolate: 3,
+  //   total: 23
+  // })
+  // Object.assign(edgeData, {
+  //   concentration: 0.75,
+  //   total: 45
+  // })
 
   loading.value = false
 }
@@ -254,13 +262,10 @@ const calculateLayout = () => {
       }
 
       // 计算绝对位置(无响应式更新)
-      console.log(btn)
-      console.log(containerWidth, COLUMN_GAP, MAX_COLUMNS)
       const pos = {
         x: 20 + btn.col * COLUMN_GAP,
         y: btn.row * ROW_GAP
       }
-      console.log(pos)
 
       // 直接赋值避免触发响应式更新
       if (btn.position.x !== pos.x || btn.position.y !== pos.y) {
@@ -270,7 +275,7 @@ const calculateLayout = () => {
     })
     setTimeout(() => {
       updateConnections()
-    }, 200)
+    }, 500)
 
   })
 }
@@ -282,13 +287,13 @@ const hideExtraBtns = (index) => {
 
 // 获取按钮中心坐标
 const getButtonCenter = (btn) => {
-  const index = buttons.value.findIndex(b => b.id === btn.id)
-  if (index === -1 || !buttonRefs.value[index]) return { x: 0, y: 0 }
 
-  const el = buttonRefs.value[index]
+  const el = buttonRefs.value.find(b => b.__vnode.key === "btn-" + btn.id)
+  if (!el) return { x: 0, y: 0 }
+
   const containerRect = flowContainer.value.getBoundingClientRect()
   const btnRect = el.getBoundingClientRect()
-
+  console.log(btn.id, containerRect, btnRect)
   return {
     x: btnRect.left - containerRect.left + el.offsetWidth / 2,
     y: btnRect.top - containerRect.top + el.offsetHeight / 2,
@@ -303,9 +308,10 @@ const getButtonCenter = (btn) => {
 
 // 更新连接线
 const updateConnections = () => {
-  connections.value = []
 
+  connections.value = []
   // 遍历所有按钮建立连接关系
+  console.log(buttons)
   buttons.value.forEach(btn => {
     if (btn.parentId) {
       const parent = buttons.value.find(b => b.id === btn.parentId)
@@ -313,8 +319,13 @@ const updateConnections = () => {
 
       // 获取实际渲染位置
       console.log("redraw line", btn.id, parent.id)
-      const start = getButtonCenter(parent)
-      const end = getButtonCenter(btn)
+      let start = getButtonCenter(parent)
+      let end = getButtonCenter(btn)
+      // 解决颠倒问题
+      // if(start.top > end.top){
+      //   start = getButtonCenter(btn)
+      //   end = getButtonCenter(parent)
+      // }
       // 除初始节点的并行外,均为父下到子上
       if (btn.row !== 0) {
         if (start.x > 0 && start.y > 0 && end.x > 0 && end.y > 0) {
@@ -411,13 +422,20 @@ const canExpandBottom = (btn) => {
 
 
 // 添加新按钮
-const addButton = (parentIndex, direction) => {
-  let parent = buttons.value[parentIndex]
+// 可以同时传入数据,直接完成构建
+const addButton = (parentIndex, direction, button = null) => {
+  let parent = null
+  if (button) {
+    parent = buttons.value.find(p => p.id === button.parent)
+  } else {
+    parent = buttons.value[parentIndex]
+  }
   if (!parent.algorithm) {
     ElMessage.error("必须先选择当前流程算法")
     return
   }
-  const newId = Date.now()
+  // 如果传入数据,则使用数据中的值
+  const newId = button ? button.id : Date.now()
   let selfCol = parent.col
   let selfRow = parent.row
   let allowCreate = true
@@ -425,7 +443,7 @@ const addButton = (parentIndex, direction) => {
   let allowedAlgorithms = []
   if (direction == 'right') {
     // 创建并行节点时,从父节点的父节点开始
-    // 初始节点有更上一级
+    // 初始节点有更上一级
     if (parent.parentId) {
       parent = buttons.value.find(b => b.id === parent.parentId)
     }
@@ -475,14 +493,14 @@ const addButton = (parentIndex, direction) => {
 
   const newBtn = {
     id: newId,
-    label: '请选择处理算法',
-    parentId: parent.id,
+    label: button ? algorithms.value.find(p => p.value === button.algorithm).label : '请选择处理算法',
+    parentId: button ? button.parent : parent.id,
     direction,
     col: selfCol,
     row: selfRow,
     position: { ...parent.position },
     children: [],
-    algorithm: null,
+    algorithm: button ? algorithms.value.find(p => p.value === button.algorithm) : null,
     algorithms: allowedAlgorithms,
   }
 
@@ -554,15 +572,15 @@ const submitPlan = async () => {
     }
   }
   try {
-    const response = await postData('/plan/', { mission: mission.id, plans: plans })
+    const response = await postData('/plan/', { mission: mission.value.id, plans: plans })
     planning.value = false
     calculating.value = true
-    if(response.message === "规划提交成功"){
+    if (response.message === "规划提交成功") {
       ElMessage.success('规划已提交')
-    }else if(response.message === "规划已覆盖"){
+    } else if (response.message === "规划已覆盖") {
       ElMessage.warning('该计算任务规划已被覆盖')
     }
-    response.data.forEach( p => {
+    response.data.forEach(p => {
       const btn = buttons.value.find(btn => btn.col === p.col && btn.row === p.row)
       btn.id = p.id
       btn.parentId = p.parentId
@@ -571,12 +589,19 @@ const submitPlan = async () => {
     const newPath = currentPath.endsWith('/') ? currentPath + 'calculate' : currentPath + '/calculate';
     router.push(newPath)
   } catch (e) {
-      ElMessage.error(e.response.data.message)
+    ElMessage.error(e.response.data.message)
   }
 }
 
+
 // 初始化
-onMounted(() => {
+onMounted(async () => {
+  console.log("Mounted")
+  try {
+    await fetchData()
+  } finally {
+    loading.value = false
+  }
   const resizeObserver = new ResizeObserver(() => {
     calculateLayout()
   })
@@ -585,6 +610,110 @@ onMounted(() => {
   }
 })
 
+const route = useRoute();
+// 监听路由变化
+watch(
+  () => route.path,
+  async (newPath) => {
+    // 路由变化时,修改显示内容
+    if (newPath === '/dashboard/analyze/plan/calculate' || newPath === '/dashboard/analyze/plan') {
+      // 进入计算页面,尝试获取plan数据,该数据需存入buttons响应数据中
+      // 检查buttons中是否有数据
+      if (buttons.value.length === 1 && buttons.value[0].label === "请选择处理算法") {
+        console.log("缓存中没有数据")
+        // 缓存中没有数据,需要重新获取
+        try {
+          if (mission.value.id === null) {
+            fetchData()
+          }
+          const response = await getData('/plan/', { mission: mission.value.id })
+          const rootBtn = response.data.find(p => p.parent === null)
+          let findStack = [rootBtn]
+          let row = 0
+          while (findStack) {
+            // 初始化列数
+            let col = 0
+            // 记录前一个节点
+            let pre = null
+            let parent = findStack.pop()
+            if (parent === null || parent === undefined) {
+              break
+            }
+            response.data.filter(p => p.parent === parent.id).forEach(p => {
+              findStack.push(p)
+              let algorithm = algorithms.value.find(algo => algo.value === p.algorithm)
+              // 第一行第一列节点,直接修改初始节点
+              if (row === 0 && col === 0) {
+                buttons.value[0].id = p.id
+                buttons.value[0].algorithm = algorithm
+                buttons.value[0].label = algorithm.label
+                // 同时修改列计数第一列被当前节点占据
+                colRecord.value[0] = p.id
+              } else {
+                // 后续节点,同一个parent,在panrent的下方水平扩展
+                if (col == 0) {
+                  // 子节点中第一个,在parent的下方扩展
+                  addButton(p.parent, 'bottom', p)
+                } else {
+                  // 子节点的后续,在第一个子节点的水平扩展
+                  addButton(pre.id, 'right',)
+                }
+              }
+              // 保存上一个节点
+              pre = p
+              // 横向序号
+              col += 1
+            })
+            //过了第一个行,后续row全部不等于0,注意此时row已经失真
+            row += 1
+          }
+
+          ElMessage.success("规划任务恢复成功")
+          // 切换route-view
+          if (newPath === '/dashboard/analyze/plan') {
+            planning.value = true
+            calculating.value = false
+          } else {
+            planning.value = false
+            calculating.value = true
+          }
+        } catch (error) {
+          console.log(error)
+          ElMessage.error("获取规划任务数据出错,返回规划页面")
+          planning.value = true
+          calculating.value = false
+          router.push('/dashboard/analyze/plan')
+        }
+      } else {
+        // 缓存中有数据,不做处理
+        // 切换route-view
+        if (newPath === '/dashboard/analyze/plan') {
+          calculateLayout()
+          planning.value = true
+          calculating.value = false
+        } else {
+          planning.value = false
+          calculating.value = true
+        }
+
+      }
+    }
+    // if (newPath === '/dashboard/analyze/plan') {
+    //   try {
+    //     await fetchData()
+    //   } finally {
+    //     loading.value = false
+    //   }
+    //   const resizeObserver = new ResizeObserver(() => {
+    //     calculateLayout()
+    //   })
+    //   if (flowContainer.value) {
+    //     resizeObserver.observe(flowContainer.value)
+    //   }
+    // }
+  },
+  { immediate: true }
+);
 </script>
 
 <style scoped>

+ 3 - 10
viewer/src/views/dashoard/select.vue

@@ -12,7 +12,7 @@
       <div class="button-column right-column">
         <div class="action-button" @click="navigateTo('view')" @mouseenter="hoverRight = true"
           @mouseleave="hoverRight = false">
-          <View></View>
+          <View class="main-icon"></View>
           <div class="button-text">查看计算任务</div>
         </div>
       </div>
@@ -67,8 +67,8 @@ onMounted(() => {
   flex-direction: column;
 
   .main-icon {
-    width: 40% !important; // 从50%减小到40%
-    height: 40% !important;
+    width: 20% !important; // 从50%减小到40%
+    height: 20% !important;
     margin-bottom: 12px; // 增加与文字的间距
   }
 
@@ -77,7 +77,6 @@ onMounted(() => {
     flex: 1;
     display: flex;
     width: 100%;
-    padding: 0 20px 0 20px;
     box-sizing: border-box;
 
     .button-column {
@@ -111,11 +110,6 @@ onMounted(() => {
           color: #606266;
           font-weight: 500;
         }
-
-        svg {
-          width: 40%;
-          height: 40%;
-        }
       }
     }
   }
@@ -124,7 +118,6 @@ onMounted(() => {
     flex-shrink: 0;
     height: 25vh;
     display: flex;
-    padding: 0 20px 0 20px;
     justify-content: center;
 
     .button-column {

+ 8 - 3
viewer/src/views/dashoard/taskfile.vue

@@ -31,7 +31,7 @@
   </template>
   
   <script setup>
-  import { ref, onMounted, computed , inject} from 'vue'
+  import { ref, onMounted, computed , inject, onActivated} from 'vue'
   import { ElMessage } from 'element-plus' 
   import { getData , postData } from '@/api/axios.js'
 
@@ -66,11 +66,16 @@ const items = ref([])
     }
   }
 
-  //请求获取数据
+  // 请求获取数据
   onMounted(() => {
     fetchTasks()
   })
-  
+
+  // 后退时重新获取数据
+  onActivated(() => {
+    console.log("BACK TO TASKVIEW")
+    fetchTasks()
+  })
   // 分页计算属性
 const currentPage = ref(1)
 const pageSize = ref(10)

+ 2 - 2
viewer/src/views/dashoard/threeDView.vue

@@ -34,7 +34,7 @@ import * as THREE from 'three';
 import html2canvas from 'html2canvas';
 
 const props = defineProps({
-    result: {
+    plan: {
         type: Number,
         required: true,
     }
@@ -415,7 +415,7 @@ onMounted(() => {
     window.addEventListener('resize', updateContainerRect)
     window.addEventListener('scroll', updateContainerRect, true)
     // 根据result的id获取图像结果
-    getData('/generateGraph', { method: 'web', result: props.result }).then(response => {
+    getData('/generateGraph', { method: 'web', plan: props.plan }).then(response => {
         nodes.value = []
         edges.value = []
         response.data.nodes.forEach(node => {

+ 53 - 32
viewer/src/views/dashoard/view.vue

@@ -57,8 +57,8 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed, onMounted, inject } from 'vue'
-import { useRouter } from 'vue-router'
+import { ref, reactive, computed, onMounted, inject, onActivated, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
 import { Calendar, User, Connection } from '@element-plus/icons-vue'
 import { postData, getData } from '@/api/axios'
 import { ElMessage } from 'element-plus'
@@ -81,7 +81,7 @@ const statusText = {
   'calculating': '计算进行中',
   'done': '计算已完成',
 }
-// 用于区分显示missions或plan
+// 用于控制点击后跳转到plan页面
 const showMissions = ref(false)
 
 // 分页配置
@@ -119,19 +119,28 @@ const formatTime = (isoString) => {
 // 路由跳转
 const router = useRouter()
 const handleCardClick = (id) => {
-  preparePlan(missions.value.find(mission => mission.id == id))
-  // 切换显示plan的routeview
-  showMissions.value = false
-  router.push(`/dashboard/view/plan`)
+  // 检查mission的状态,如果还未计算,则应进入plan界面
+  // 如果已经计算,应进入calculate界面
+  let mission = missions.value.find(mission => mission.id == id)
+  preparePlan(mission)
+  if(mission.status == "init"){
+    // 初始状态,应进入流程规划
+    // 切换显示plan的routeview
+    showMissions.value = false
+    router.push(`/dashboard/view/plan`)
+  }else{
+    // 否则进入calculate界面,可查看计算过程或结果
+    router.push(`/dashboard/view/plan/calculate`)
+  } 
 }
 
 const preparePlan = (mission) => {
   console.log("选中的Mission:")
   console.log(mission)
   useAnalyzeInfo.analyzeInfo.value.nodeFile.amount = mission.nodesInfo.S + mission.nodesInfo.D + mission.nodesInfo.I
-  useAnalyzeInfo.analyzeInfo.value.nodeFile.sNode = mission.nodesInfo.S
-  useAnalyzeInfo.analyzeInfo.value.nodeFile.dNode = mission.nodesInfo.D
-  useAnalyzeInfo.analyzeInfo.value.nodeFile.iNode = mission.nodesInfo.I
+  useAnalyzeInfo.analyzeInfo.value.nodeFile.sNodes = mission.nodesInfo.S
+  useAnalyzeInfo.analyzeInfo.value.nodeFile.dNodes = mission.nodesInfo.D
+  useAnalyzeInfo.analyzeInfo.value.nodeFile.iNodes = mission.nodesInfo.I
 
   useAnalyzeInfo.analyzeInfo.value.edgeFile.amount = mission.edgesInfo.num
   useAnalyzeInfo.analyzeInfo.value.mission = {
@@ -144,32 +153,44 @@ const preparePlan = (mission) => {
 }
 
 //加载数据
-onMounted(async () => {
-  // 获取missions
-  missions.value = []
-  // 显示missions
-  showMissions.value = true
+const loadMissions = async () => {
+  missions.value = [];
+  showMissions.value = true;
   try {
-    const response = await getData('/missions/')
-    response.data.forEach(mission => {
-      missions.value.push({
-        id: mission.id,
-        name: mission.name,
-        createTime: mission.createTime,
-        nodesInfo: mission.nodesInfo,
-        edgesInfo: mission.edgesInfo,
-        status: mission.status,
-        description: "This is description",
-      })
-    })
-    pagination.value.total = missions.value.length
+    const response = await getData('/missions/');
+    missions.value = response.data.map(mission => ({
+      id: mission.id,
+      name: mission.name,
+      createTime: mission.createTime,
+      nodesInfo: mission.nodesInfo,
+      edgesInfo: mission.edgesInfo,
+      status: mission.status,
+      // 需要后续完善描述输入
+      description: "This is description",
+    }));
+    pagination.value.total = missions.value.length;
   } catch (error) {
-    console.log(error)
-    ElMessage.error("获取任务列表错误")
+    console.error(error);
+    ElMessage.error("获取任务列表错误");
   }
-})
-
+}
 
+const route = useRoute();
+// 监听路由变化
+watch(
+  () => route.path,
+  (newPath) => {
+    if (newPath === '/dashboard/view' || newPath === '/dashboard/taskfile') {
+      showMissions.value = true;
+    }
+  },
+  { immediate: true }
+);
+
+// 首次加载
+onMounted(loadMissions);
+// 通过后退按钮返回页面时重新加载
+onActivated(loadMissions);
 
 </script>
 

+ 4 - 0
viewer/vite.config.ts

@@ -15,4 +15,8 @@ export default defineConfig({
       '@': fileURLToPath(new URL('./src', import.meta.url))
     },
   },
+  server: {
+    host: '127.0.0.1',
+    port: 5173,
+  }
 })