2 Commits c23c021f53 ... ba5e05278a

Tác giả SHA1 Thông báo Ngày
  Lan ba5e05278a 4-3更新 1 tháng trước cách đây
  Lan a8a6814767 '添加图相关' 1 tháng trước cách đây
44 tập tin đã thay đổi với 1646 bổ sung34 xóa
  1. BIN
      backend/api/__pycache__/api_graph.cpython-310.pyc
  2. BIN
      backend/api/__pycache__/api_rawDataTrans.cpython-310.pyc
  3. BIN
      backend/api/__pycache__/urls.cpython-310.pyc
  4. 280 0
      backend/api/api_graph.py
  5. 3 0
      backend/api/api_rawDataTrans.py
  6. 85 0
      backend/api/migrations/0017_graph_graphtoken.py
  7. 37 0
      backend/api/migrations/0018_rename_edgemap_graph_edges_and_more.py
  8. 24 0
      backend/api/migrations/0019_alter_graph_user.py
  9. BIN
      backend/api/migrations/__pycache__/0017_graph_graphtoken.cpython-310.pyc
  10. BIN
      backend/api/migrations/__pycache__/0018_rename_edgemap_graph_edges_and_more.cpython-310.pyc
  11. BIN
      backend/api/migrations/__pycache__/0019_alter_graph_user.cpython-310.pyc
  12. 2 1
      backend/api/models/__init__.py
  13. BIN
      backend/api/models/__pycache__/__init__.cpython-310.pyc
  14. BIN
      backend/api/models/__pycache__/algorithm.cpython-310.pyc
  15. BIN
      backend/api/models/__pycache__/file.cpython-310.pyc
  16. BIN
      backend/api/models/__pycache__/graph.cpython-310.pyc
  17. BIN
      backend/api/models/__pycache__/mission.cpython-310.pyc
  18. 6 0
      backend/api/models/algorithm.py
  19. 35 7
      backend/api/models/file.py
  20. 464 0
      backend/api/models/graph.py
  21. 3 0
      backend/api/urls.py
  22. BIN
      backend/db.sqlite3
  23. 0 0
      scheduler/process_logs/proc_20250327-220139_13732.stderr
  24. 7 0
      scheduler/process_logs/proc_20250327-220139_13732.stdout
  25. 0 0
      scheduler/process_logs/proc_20250327-220209_13732.stderr
  26. 7 0
      scheduler/process_logs/proc_20250327-220209_13732.stdout
  27. 0 0
      scheduler/process_logs/proc_20250401-001805_21488.stderr
  28. 7 0
      scheduler/process_logs/proc_20250401-001805_21488.stdout
  29. 0 0
      scheduler/process_logs/proc_20250401-001834_21488.stderr
  30. 7 0
      scheduler/process_logs/proc_20250401-001834_21488.stdout
  31. 0 0
      scheduler/process_logs/proc_20250401-004055_21488.stderr
  32. 7 0
      scheduler/process_logs/proc_20250401-004055_21488.stdout
  33. 0 0
      scheduler/process_logs/proc_20250401-004125_21488.stderr
  34. 7 0
      scheduler/process_logs/proc_20250401-004125_21488.stdout
  35. 0 0
      scheduler/process_logs/proc_20250401-004232_21488.stderr
  36. 7 0
      scheduler/process_logs/proc_20250401-004232_21488.stdout
  37. 0 0
      scheduler/process_logs/proc_20250401-004302_21488.stderr
  38. 7 0
      scheduler/process_logs/proc_20250401-004302_21488.stdout
  39. 3 0
      viewer/package.json
  40. 39 0
      viewer/pnpm-lock.yaml
  41. 28 0
      viewer/src/views/dashoard/VRView.vue
  42. 159 26
      viewer/src/views/dashoard/calculate.vue
  43. 213 0
      viewer/src/views/dashoard/chartView.vue
  44. 209 0
      viewer/src/views/dashoard/threeDView.vue

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


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


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


+ 280 - 0
backend/api/api_graph.py

@@ -0,0 +1,280 @@
+from django.contrib import auth
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status
+from rest_framework.authtoken.models import Token
+from rest_framework.authentication import BasicAuthentication, TokenAuthentication
+from .serializers import UserRegisterSerializer
+
+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 random import randint
+
+import json, csv
+
+class ViewGraphByToken(APIView):
+    # 通过验证码看图——供VR使用
+    authentication_classes = []
+    permission_classes = []
+    def get(self, request):
+        print(request.GET)
+        requestToken = request.GET.get('token')
+        print(requestToken)
+        try:
+            token = GraphToken.objects.get(token=str(requestToken))
+        except GraphToken.DoesNotExist:
+            return failed(message="Token不存在")
+        if token.checkExpire():
+            return failed(message="Token已过期")
+        graph = token.graph
+        return success(data={
+            'nodes': graph.nodes,
+            'edges': graph.edges,
+        })
+
+
+class GenerateGraph(APIView):
+    # 生成图数据
+    def get(self, request):
+        user = request.user
+        method = request.GET.get('method') # 以何种视图查看
+        resultId = request.GET.get('result')
+        # 测试用,固定result值,省略计算
+        resultId = 57
+        # 检查result的传递值是否正确
+        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)
+
+        # 图表显示不生成图,仅做统计计算
+        if method == 'chart':
+            nodeJson = result.nodeFile.toJson()
+            edgeJson = result.edgeFile.toJson()
+            def isOriginEdge(edge):
+                for meta in edge['meta']:
+                    if meta.get('optimize') == 'new' or meta.get('predict') == 'new':
+                        return False
+                return True
+            # 需要完善图表视图统计数据,根据具体图表类型决定
+            # 通用图数据统计,包括SDI节点数量、边数量,各类型节点度数分布
+            # 平均链长度,聚类系数即D节点间连接紧密程度
+            # 分别统计SDI节点的数量和度数:sNodes,dNodes,iNodes 
+            sDegree = dDegree = iDegree = [0 for i in range(11)] # 度的取值范围0~10,共11个元素
+            sNodes = [node for node in nodeJson if node['type'] == 'S']
+            for s in sNodes:
+                degree = len([edge for edge in edgeJson if edge['from'] == s['id'] or edge['to'] == s['id']])
+                if degree <= 10:
+                    sDegree[degree] += 1
+                else:
+                    sDegree[10] += 1
+            dNodes = [node for node in nodeJson if node['type'] == 'D']
+            for d in dNodes:
+                degree = len([edge for edge in edgeJson if edge['from'] == d['id'] or edge['to'] == d['id']])
+                if degree <= 10:
+                    dDegree[degree] += 1
+                else:
+                    dDegree[10] += 1
+            iNodes = [node for node in nodeJson if node['type'] == 'I']
+            for i in iNodes:
+                degree = len([edge for edge in edgeJson if edge['from'] == i['id'] or edge['to'] == i['id']])
+                if degree <= 10:
+                    iDegree[degree] += 1
+                else:
+                    iDegree[10] += 1
+            # 查找所有OODA链路:chains
+            chains = []
+            for s in sNodes:
+                stack = [(s, [s])]  # (当前节点, 路径, 已访问节点)
+                while stack:
+                    current, path = stack.pop()
+                    # 必须限制链路长度,否则无法结束,除限制长度外还可尝试将不同顺序节点的链路视为同一条,从而减少链路数量
+                    if len(path) > 5:
+                        continue
+                    # 终止条件:到达I节点
+                    if current['type'] == 'I':
+                        chains.append(path)
+                        continue
+                    
+                    # 遍历邻居,注意通用计算只统计原始图
+                    neighbors = [edge['to'] for edge in edgeJson if (isOriginEdge(edge) and edge['from'] == current['id'])] + [edge['from'] for edge in edgeJson if (isOriginEdge(edge) and edge['to'] == current['id'])]
+                    for neighborId in neighbors:
+                        neighbor = [node for node in nodeJson if node.get('id') == neighborId]
+                        if len(neighbor) != 1:
+                            return failed(message="查找OODA链路时出错:不存在的邻居节点")
+                        neighbor = neighbor[0]
+                        if neighbor in path: continue  # 避免环路
+                        # 不能连线的几种情况
+                        if current['type'] == 'S' and neighbor['type'] == 'I':
+                            continue
+                        if current['type'] == 'D' and neighbor['type'] == 'S':
+                            continue
+                        stack.append((neighbor, path + [neighbor]))
+            chainsNum = len(chains)
+            chainsNumByLength = [0 for i in range(5)]
+            for chain in chains:
+                length = len(chain)
+                if length >= 5:
+                    length = 5
+                chainsNumByLength[length-1] += 1
+            averageChainsLength = round(sum([len(chain) for chain in chains]) / chainsNum, 2)
+            data = {
+                'sNodesNum': len(sNodes),
+                'dNodesNum': len(dNodes),
+                'iNodesNum': len(iNodes),
+                'sDegree': sDegree,
+                'dDegree': dDegree,
+                'iDegree': iDegree,
+                'chains': {
+                    'num': chainsNum,
+                    'averageLength': averageChainsLength,
+                    'numByLength': chainsNumByLength,
+                },
+            }
+            # 根据计算类型的不同分别计算特殊数据
+            if result.plan.algorithm.type in ['optimize', 'predict'] :
+                # 拓扑优化算法应该统计原有图的数据与优化后图的数据,优化应该仅优化边
+                newChains = []
+                for s in sNodes:
+                    stack = [(s, [s])]  # (当前节点, 路径, 已访问节点)
+                    while stack:
+                        current, path = stack.pop() 
+                        
+                        # 终止条件:到达I节点
+                        if current['type'] == 'I':
+                            newChains.append(path)
+                            continue
+                        
+                        # 遍历邻居,注意优化计算只统计新图
+                        neighbors = [edge['to'] for edge in edgeJson if (not isOriginEdge(edge) and edge['from'] == current['id'])] + [edge['from'] for edge in edgeJson if (not isOriginEdge(edge) and edge['to'] == current['id'])]
+                        for neighborId in neighbors:
+                            neighbor = [node for node in nodeJson if node.get('id') == neighborId]
+                            if len(neighbor) != 1:
+                                return failed(message="查找OODA链路时出错:不存在的邻居节点")
+                            neighbor = neighbor[0]
+                            if neighbor in path: continue  # 避免环路
+                            # 不能连线的几种情况
+                            if current['type'] == 'S' and neighbor['type'] == 'I':
+                                continue
+                            if current['type'] == 'D' and neighbor['type'] == 'S':
+                                continue
+                            stack.append((neighbor, path + [neighbor]))
+                newChainsNum = len(newChains)
+                newAverageChainsLength = 0
+                if newChainsNum != 0:
+                    newAverageChainsLength = round(sum([len(chain) for chain in newChains]) / newChainsNum, 2)
+                data['newChains'] = {
+                        'num': newChainsNum,
+                        'averageLength': newAverageChainsLength,
+                    }                        
+            return success(data=data)
+
+        # 如果已经生成过图数据,则直接生成新的验证码
+        if result.own_graphs.exists():
+            graph = result.own_graphs.first()
+            if method == 'web' or method == '3D' or method == '3d':
+                return success(data={
+                    'nodes': graph.nodes,
+                    'edges': graph.edges,
+                })
+            elif method == 'VR':
+                token = graph.generateToken()
+                return success(data={
+                    'token': token,
+                })
+        else:
+            nodeJson = result.nodeFile.toJson()
+            edgeJson = result.edgeFile.toJson()
+
+
+######测试用,添加几个标签
+            for node in nodeJson:
+                node['meta'].append({'optimize': 'new'})
+                node['meta'].append({'group': randint(1,5)})
+                node['meta'].append({'predict': 'new'})
+            for edge in edgeJson:
+                edge['meta'].append({'optimize': 'new'})
+                edge['meta'].append({'predict': 'new'})
+########################
+
+
+            # 两种3D视图的数据结构应该是一样的
+            if result.plan.algorithm.type == 'optimize':
+                # 拓扑优化算法生成的结果,只有网络结构数据
+                # 检查合法性
+                for node in nodeJson:
+                    if not 'id' in node or not 'type' in node:
+                        return failed(message="拓扑优化结果的节点文件存在问题")
+                for edge in edgeJson:
+                    if not 'from' in edge or not 'to' in edge:
+                        return failed(message="边文件存在问题")
+                    # 对于优化算法,边的属性中应该有新旧的区分
+                    missingLabel = True
+                    for meta in edge['meta']:
+                        if 'optimize' in meta and meta['optimize'] in ['new', 'old']:
+                            missingLabel = False
+                    if missingLabel:
+                        return failed(message="无拓扑优化标签")
+            elif result.plan.algorithm.type == 'group':
+                # 功能体探测算法生成的结果
+                # 检查合法性
+                for node in nodeJson:
+                    if not 'id' in node or not 'type' in node:
+                        return failed(message="节点文件存在问题")
+                    # 对于功能体探测算法,节点的属性中应有功能体的编号
+                    missingLabel = True
+                    for meta in node['meta']:
+                        if 'group' in meta and type(meta['group']) == int:
+                            missingLabel = False
+                    if missingLabel:
+                        return failed(message="无功能体标签")
+                for edge in edgeJson:
+                    if not 'from' in edge or not 'to' in edge:
+                        return failed(message="边文件存在问题")
+            elif result.plan.algorithm.type == 'predict':
+                # 链路演化预测算法生成的结果
+                # 检查合法性
+                for node in nodeJson:
+                    if not 'id' in node or not 'type' in node:
+                        return failed(message="节点文件存在问题")
+                    # 对于演化预测算法,节点的属性中应有新旧区分
+                    missingLabel = True
+                    for meta in node['meta']:
+                        if 'predict' in meta and meta['predict'] in ['new', 'old']:
+                            missingLabel = False
+                    if missingLabel:
+                        return failed(message="无演化预测标签")
+                for edge in edgeJson:                 
+                    if not 'from' in edge or not 'to' in edge:
+                        return failed(message="边文件存在问题")
+                    # 对于演化预测算法,边的属性中应有新旧区分
+                    missingLabel = True
+                    for meta in edge['meta']:
+                        if 'predict' in meta and meta['predict'] in ['new', 'old']:
+                            missingLabel = False
+                    if missingLabel:
+                        return failed(message="无演化预测标签")
+            graph = Graph.objects.createFromResult(result)
+            graph.save()
+            if method == 'web':
+                return success(data={
+                    'nodes': graph.nodes,
+                    'edges': graph.edges,
+                })
+            if method == 'VR':
+                token = graph.generateToken()
+                return success(data={
+                    'token': token,
+                })
+

+ 3 - 0
backend/api/api_rawDataTrans.py

@@ -28,7 +28,9 @@ class RawDataTrans(APIView):
             # mission = Mission.objects.get(id=int(request.data.get('missionId')))
             plan = Plan.objects.get(id=int(request.data.get('planId')))
             return success("测试返回图数据")
+        
     def post(self, request):
+        # 处理进程反馈计算结果
         mission = Mission.objects.get(id=int(request.data.get('missionId')))
         plan = Plan.objects.get(id=int(request.data.get('planId')))
         nodes = request.data.get('nodes')
@@ -69,6 +71,7 @@ class RawDataTrans(APIView):
                 result.edgeFile = edgeFile
                 result.progress = 100
                 result.save()
+
             else:
                 # 进度不到百分百,正在执行中,仅更新进度数值
                 result.progress = int(progress)

+ 85 - 0
backend/api/migrations/0017_graph_graphtoken.py

@@ -0,0 +1,85 @@
+# Generated by Django 4.2 on 2025-03-25 11:43
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("api", "0016_alter_result_edgefile_alter_result_nodefile"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="Graph",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("create_time", models.DateTimeField(auto_now_add=True)),
+                ("update_time", models.DateTimeField(auto_now=True)),
+                (
+                    "type",
+                    models.CharField(
+                        choices=[
+                            ("optimize", "optimize"),
+                            ("group", "group"),
+                            ("predict", "predict"),
+                        ],
+                        default="optimize",
+                        max_length=16,
+                    ),
+                ),
+                ("nodesMap", models.JSONField()),
+                ("edgeMap", models.JSONField()),
+                (
+                    "result",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="own_graphs",
+                        to="api.result",
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="own_graphs",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+            ],
+        ),
+        migrations.CreateModel(
+            name="GraphToken",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("create_time", models.DateTimeField(auto_now_add=True)),
+                ("token", models.CharField(max_length=8)),
+                (
+                    "graph",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="own_tokens",
+                        to="api.graph",
+                    ),
+                ),
+            ],
+        ),
+    ]

+ 37 - 0
backend/api/migrations/0018_rename_edgemap_graph_edges_and_more.py

@@ -0,0 +1,37 @@
+# Generated by Django 4.2 on 2025-03-27 22:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("api", "0017_graph_graphtoken"),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name="graph",
+            old_name="edgeMap",
+            new_name="edges",
+        ),
+        migrations.RenameField(
+            model_name="graph",
+            old_name="nodesMap",
+            new_name="nodes",
+        ),
+        migrations.AddField(
+            model_name="algorithm",
+            name="type",
+            field=models.CharField(
+                choices=[
+                    ("optimize", "optimize"),
+                    ("group", "group"),
+                    ("predict", "predict"),
+                ],
+                default="optimize",
+                max_length=16,
+            ),
+            preserve_default=False,
+        ),
+    ]

+ 24 - 0
backend/api/migrations/0019_alter_graph_user.py

@@ -0,0 +1,24 @@
+# Generated by Django 4.2 on 2025-03-27 23:55
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("api", "0018_rename_edgemap_graph_edges_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="graph",
+            name="user",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="user_own_graphs",
+                to=settings.AUTH_USER_MODEL,
+            ),
+        ),
+    ]

BIN
backend/api/migrations/__pycache__/0017_graph_graphtoken.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0018_rename_edgemap_graph_edges_and_more.cpython-310.pyc


BIN
backend/api/migrations/__pycache__/0019_alter_graph_user.cpython-310.pyc


+ 2 - 1
backend/api/models/__init__.py

@@ -4,4 +4,5 @@ from .view import View
 from .mission import Mission
 from .result import Result
 from .plan import Plan
-from .algorithm import Algorithm
+from .algorithm import Algorithm
+from .graph import Graph, GraphToken

BIN
backend/api/models/__pycache__/__init__.cpython-310.pyc


BIN
backend/api/models/__pycache__/algorithm.cpython-310.pyc


BIN
backend/api/models/__pycache__/file.cpython-310.pyc


BIN
backend/api/models/__pycache__/graph.cpython-310.pyc


BIN
backend/api/models/__pycache__/mission.cpython-310.pyc


+ 6 - 0
backend/api/models/algorithm.py

@@ -2,6 +2,11 @@ from django.db import models
 import os, errno
 
 from api.utils import *
+algoType = [
+    ('optimize', 'optimize'),
+    ('group', 'group'),
+    ('predict', 'predict'),
+]
 
 class AlgorithmManager(models.Manager):
     def statistic(self, user):
@@ -12,6 +17,7 @@ class AlgorithmManager(models.Manager):
 
 class Algorithm(models.Model):
     name = models.CharField(default="", max_length=32)
+    type = models.CharField(choices=algoType, max_length=16)
     create_time = models.DateTimeField(auto_now_add=True)
     update_time = models.DateTimeField(auto_now=True)
     

+ 35 - 7
backend/api/models/file.py

@@ -2,6 +2,8 @@ from django.db import models
 import os, errno
 import csv
 from api.utils import *
+import json
+from random import randint
 
 types = [
     ('csv', 'csv'),
@@ -25,7 +27,7 @@ BASE_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__
 class FileManager(models.Manager):
     def getHistory(self, user):
         # try:
-        files = user.own_files.all()
+        files = user.own_files.filter(usage="input").all()
         history = []
         for file in files:
             fileId = file.id
@@ -107,6 +109,7 @@ class File(models.Model):
         self.save()
 
     def generate(self, data):
+        # 从json结果生成文件
         path = os.path.join(BASE_FILE_PATH, str(self.user.id))
         if os.path.exists(os.path.join(path, str(self.id))):
             self.delete()
@@ -117,9 +120,6 @@ class File(models.Model):
                 file = open(os.path.join(path, str(self.id)), 'w', newline='')
                 csvFile = csv.writer(file)
                 for line in data:
-                    if not len(line) == 2:
-                        print("check file illegal failed", "node", "len = 2")
-                        return FAILED
                     if not str(line[0]).isdigit():
                         print("check file illegal failed", "node", "id wrong")
                         return FAILED
@@ -131,6 +131,7 @@ class File(models.Model):
                     else:
                         print("check file illegal failed", "node", "dudplicate id")
                         return FAILED
+                    # 除了节点编号和节点类型外,其余参数全部放在line的后续位置,以字符串json的格式保存
                     csvFile.writerow(line)
                 file.close()
                 return OK
@@ -139,11 +140,13 @@ class File(models.Model):
                 file = open(os.path.join(path, str(self.id)), 'w', newline='')
                 csvFile = csv.writer(file)
                 for line in data:
-                    if not len(line) == 2:
+                    if not str(line[0]).isdigit() or not str(line[1]).isdigit():
                         print("check file illegal failed", "edge", "len =2")
                         return FAILED
+                    # 注意默认将边视为无向边
                     if [line[0], line[1]] not in edges and [line[1], line[0]] not in edges:
                         edges.append([line[0], line[1]])
+                    # 后续参数放在line的后续位置
                     csvFile.writerow(line)
                 file.close()
                 return OK
@@ -234,13 +237,38 @@ class File(models.Model):
             if self.type == 'csv':
                 nodes = []
                 for line in file:
-                    nodes.append({'id': line[0], 'type': line[1]})
+                    # 如果有额外数据,则放入第三个字段中
+                    node = {'id': line[0], 'type': line[1], 'meta': []}
+                    for el in range(2, len(line)):
+                        node['meta'].append(json.loads(el))
+                    
+                    # 测试用,添加optimize
+                    el = '{"optimize": "old"}'
+                    node['meta'].append(json.loads(el))
+
+                    # 测试用,添加group
+                    el = '{"group": "' + str(randint(1,5)) + '"}'
+                    node['meta'].append(json.loads(el))
+                    
+                    nodes.append(node)
+
+
                 return nodes
         if self.content == 'edge':
             if self.type == 'csv':
                 edges = []
                 for line in file:
-                    edges.append([line[0], line[1]])
+                    # 如果有额外数据,则放入第三个字段中
+                    edge = {'from': line[0], 'to': line[1], 'meta': []}
+                    for el in range(2, len(line)):
+                        edge['meta'].append(json.loads(el))
+                    
+                    # 测试用,添加optimize
+                    el = '{"optimize": "old"}'
+                    edge['meta'].append(json.loads(el))
+
+
+                    edges.append(edge)
                 return edges
 
     def deleteStorage(self):

+ 464 - 0
backend/api/models/graph.py

@@ -0,0 +1,464 @@
+from django.db import models
+from datetime import datetime
+from random import randint
+
+from api.utils import *
+
+import numpy as np
+from scipy.spatial import SphericalVoronoi
+from sklearn.preprocessing import MinMaxScaler #scikit-learn
+import networkx as nx
+from community import community_louvain #python-louvain
+
+graphForAlgo = [
+    ('optimize', 'optimize'),
+    ('group', 'group'),
+    ('predict', 'predict'),
+]
+
+
+
+class GraphManager(models.Manager):
+    def checkDuplicate(self, token):
+        try:
+            self.get(token=token)
+            return True
+        except GraphToken.DoesNotExist:
+            return False
+        return True
+
+
+class GraphToken(models.Model):
+    # 用于访问图的验证码
+    create_time = models.DateTimeField(auto_now_add=True)
+
+    graph = models.ForeignKey(to="api.Graph", on_delete=models.CASCADE, related_name="own_tokens")
+    token = models.CharField(max_length=8)
+
+    objects = GraphManager()
+
+    def checkExpire(self):
+        now = datetime.now()
+        diff = now - self.create_time
+        # 超过五分钟过期
+        return diff.total_seconds() > 300
+        
+    class Meta:
+        app_label = 'api'
+
+
+
+class GraphManager(models.Manager):
+    def statistic(self, user):
+      graphs = user.user_own_graphs.all()
+      return {
+          'amount': len(graphs),
+      }
+    
+    '''功能体探测功能的三维坐标生成 start'''
+    def createFromResultGroupAlgo(self, result):
+        print("Group3D")
+        # 参数配置
+        GROUP_SPHERE_RADIUS = 10.0  # 社团分布球体半径
+        D_CORE_RADIUS = 1.5         # D类节点核心区半径
+        SI_SHELL_RADIUS = 4.0       # S/I类节点分布半径
+        MIN_GROUP_DIST = 8.0        # 社团间最小间距
+        nodeJson = result.nodeFile.toJson()
+        edgeJson = result.edgeFile.toJson()
+        
+        # 内部函数
+        def _uniform_sphere_sampling(n, radius=1.0):
+            """均匀球面采样"""
+            points = []
+            phi = np.pi * (3.0 - np.sqrt(5.0))  # 黄金角度
+            for i in range(n):
+                y = 1 - (i / (n-1)) * 2
+                radius_at_y = np.sqrt(1 - y*y)
+                theta = phi * i
+                x = np.cos(theta) * radius_at_y
+                z = np.sin(theta) * radius_at_y
+                points.append(radius * np.array([x, y, z]))
+            return np.array(points)
+        
+        # 内部函数
+        def _optimize_layout(node_coords, groups, group_coords):
+            """带社团约束的优化"""
+            # 参数设置
+            NODE_REPULSION = 0.1    # 节点间斥力
+            GROUP_ATTRACTION = 0.05  # 社团内引力
+            ITERATIONS = 200
+            
+            ids = list(node_coords.keys())
+            positions = np.array([node_coords[id] for id in ids])
+            group_map = {id: gid for gid, data in groups.items() for id in data['D']+data['SI']}
+            
+            for _ in range(ITERATIONS):
+                # 节点间斥力
+                diffs = positions[:, None] - positions[None, :]  # 3D差分矩阵
+                dists = np.linalg.norm(diffs, axis=-1)
+                np.fill_diagonal(dists, np.inf)
+                
+                repulsion = NODE_REPULSION / (dists**2 + 1e-6)
+                repulsion[dists > 5.0] = 0  # 仅处理近距离节点
+                
+                # 社团内引力
+                attraction = np.zeros_like(positions)
+                for i, id in enumerate(ids):
+                    gid = group_map[id]
+                    center = group_coords[gid]['center']
+                    dir_to_center = center - positions[i]
+                    dist_to_center = np.linalg.norm(dir_to_center)
+                    if dist_to_center > 2*SI_SHELL_RADIUS:
+                        attraction[i] = GROUP_ATTRACTION * dir_to_center
+                
+                # 更新位置
+                movement = np.sum(repulsion[:, :, None] * diffs, axis=1) + attraction
+                positions += 0.1 * movement
+            
+            # 归一化到0-10范围
+            scaler = MinMaxScaler(feature_range=(0, 10))
+            positions = scaler.fit_transform(positions)
+            
+            return {id: tuple(pos) for id, pos in zip(ids, positions)}
+        
+        # 内部函数
+        def _generate_si_coordinates(num_points, radius):
+            # 生成随机方向
+            points = np.random.randn(num_points, 3)  # 标准正态分布采样
+            
+            # 归一化到单位球面
+            norms = np.linalg.norm(points, axis=1, keepdims=True)
+            points_normalized = points / norms
+            
+            # 缩放到目标半径
+            points_scaled = points_normalized * radius
+            return points_scaled
+
+
+        # 按group分组
+        groups = {}
+        for node in nodeJson:
+            group_id = None
+            for meta in node['meta']:
+                if 'group' in meta:
+                    group_id = meta['group']
+            if not group_id:
+                print(node, group_id, "非Group优化结果被用于进行Group图形布局生成")
+            groups.setdefault(group_id, {'D': [], 'SI': []})
+            if node['type'] == 'D':
+                groups[group_id]['D'].append(node['id'])
+            else:
+                groups[group_id]['SI'].append(node['id'])
+        
+        # === 步骤1: 为每个group分配空间位置 ===
+        group_coords = {}
+        num_groups = len(groups)
+        points = _uniform_sphere_sampling(num_groups, radius=GROUP_SPHERE_RADIUS)
+        
+        # 确保最小间距
+        for i in range(len(points)):
+            for j in range(i+1, len(points)):
+                dist = np.linalg.norm(points[i]-points[j])
+                if dist < MIN_GROUP_DIST:
+                    direction = (points[j] - points[i]) / dist
+                    points[j] = points[i] + direction * MIN_GROUP_DIST
+        
+        # 分配group中心坐标
+        for idx, (group_id, members) in enumerate(groups.items()):
+            group_coords[group_id] = {
+                'center': points[idx],
+                'D_count': len(members['D']),
+                'SI_count': len(members['SI'])
+            }
+        
+        # === 步骤2: 生成各group内部坐标 ===
+        node_coords = {}
+        for group_id, data in groups.items():
+            center = group_coords[group_id]['center']
+            
+            # D类节点:均匀分布在核心球体内
+            for node_id in data['D']:
+                r = D_CORE_RADIUS * np.random.rand()**0.5  # 密度向中心聚集
+                theta = np.random.uniform(0, 2*np.pi)
+                phi = np.arccos(2*np.random.rand() - 1)
+                
+                dx = r * np.sin(phi) * np.cos(theta)
+                dy = r * np.sin(phi) * np.sin(theta)
+                dz = r * np.cos(phi)
+                node_coords[node_id] = center + np.array([dx, dy, dz])
+            
+            # SI类节点:分布在球壳层
+            shell_radius = SI_SHELL_RADIUS + 0.5*np.abs(np.random.randn())  # 添加随机扰动
+            points = _generate_si_coordinates(len(data['SI']), shell_radius)
+            # 使用球形Voronoi分布避免重叠
+            sv = SphericalVoronoi(points, radius=shell_radius)
+            sv.sort_vertices_of_regions()
+            
+            for i, node_id in enumerate(data['SI']):
+                point = sv.points[i] * shell_radius
+                node_coords[node_id] = center + point
+        
+        # === 步骤3: 全局优化 ===
+        # return _optimize_layout(node_coords, groups, group_coords)
+        final_coords = _optimize_layout(node_coords, groups, group_coords)
+        # 将坐标添加到每个节点字典
+        for node in nodeJson:
+            node_id = node['id']
+            x, y, z = final_coords[node_id]
+            # 添加三维坐标字段
+            node['coordinates'] = {
+                'x': round(x, 4),  # 保留4位小数
+                'y': round(y, 4),
+                'z': round(z, 4)
+            }
+        print(nodeJson)
+
+        '''结果示例输出,并用三维视图显示START'''
+        # for node_id, (x, y, z) in final_coords.items():
+        #     print(f"Node {node_id}: ({x:.2f}, {y:.2f}, {z:.2f})")
+
+        # import matplotlib.pyplot as plt
+        # from mpl_toolkits.mplot3d import Axes3D
+
+        # fig = plt.figure(figsize=(10, 8))
+        # ax = fig.add_subplot(111, projection='3d')
+
+        # # 按类型绘制节点
+        # # types = {n['id']: n['meta']['group'] for n in nodeJson}
+        # types = {}
+        # for n in nodeJson:
+        #     for meta in n['meta']:
+        #         if 'group' in meta:
+        #             types[n['id']] = str(meta['group'])
+        # colors = {'1': 'red', '2': 'green', '3': 'blue', '4': 'yellow', '5': 'black'}
+
+        # for node_id, (x, y, z) in final_coords.items():
+        #     ax.scatter(x, y, z, 
+        #             c=colors[types[node_id]],
+        #             s=50 if types[node_id] == 'D' else 30,
+        #             marker='o' if types[node_id] == 'D' else '^')
+
+        # # 绘制边
+        # for edge in edgeJson:
+        #     x = [final_coords[edge['from']][0], final_coords[edge['to']][0]]
+        #     y = [final_coords[edge['from']][1], final_coords[edge['to']][1]]
+        #     z = [final_coords[edge['from']][2], final_coords[edge['to']][2]]
+        #     ax.plot(x, y, z, c='gray', alpha=0.3)
+
+        # ax.set_xlabel('X')
+        # ax.set_ylabel('Y')
+        # ax.set_zlabel('Z')
+        # plt.show()
+        '''结果示例输出,并用三维视图显示END'''
+
+
+        return self.create(
+            result=result,
+            user=result.user,
+            nodes=nodeJson,
+            edges=edgeJson,
+            type=result.plan.algorithm.type,
+        )
+    '''功能体探测功能的三维坐标生成 end'''
+
+    
+    def createFromResult(self, result):
+        nodeJson = result.nodeFile.toJson()
+        edgeJson = result.edgeFile.toJson()
+        # 功能体探测算法需要额外将同一功能体聚集,单独处理
+        if result.plan.algorithm.type == 'group':
+            return self.createFromResultGroupAlgo(result)
+        # 只有3d和VR视图需要生成图,需要给每个节点赋值一个三维坐标,该坐标需要满足一定尺度
+
+        '''START'''
+
+        # 创建NetworkX图对象
+        G = nx.Graph()
+        G.add_nodes_from([(n['id'], {'type': n['type']}) for n in nodeJson])
+        G.add_edges_from([(e['from'], e['to']) for e in edgeJson])
+
+        # 使用Louvain算法检测社团
+        partition = community_louvain.best_partition(G)
+        communities = {}
+        for node, comm_id in partition.items():
+            communities.setdefault(comm_id, []).append(node)
+
+        '''定义函数''' 
+        def generate_3d_coordinates(nodes, communities):
+            """ 计算三维空间坐标布局 """
+            # 参数设置
+            D_LAYER_RADIUS = 1.0    # D类型节点分布半径
+            I_S_LAYER_RADIUS = 3.0  # I/S类型节点分布半径, 因为D类靠中心,I/S类靠外围
+            COMMUNITY_SPACING = 8.0 # 社团间距
+            
+            # 初始化坐标字典
+            coords = {}
+            
+            # 为每个社团分配空间区域
+            comm_centers = {}
+            for i, (comm_id, members) in enumerate(communities.items()):
+                # 在三维空间分配不同象限
+                angle = i * 2*np.pi / len(communities)
+                comm_centers[comm_id] = (
+                    COMMUNITY_SPACING * np.cos(angle),
+                    COMMUNITY_SPACING * np.sin(angle),
+                    COMMUNITY_SPACING * (i % 2)  # Z轴分层
+                )
+            
+            # 为每个节点生成坐标
+            for node in nodes:
+                node_id = node['id']
+                comm_id = partition[node_id]
+                base_x, base_y, base_z = comm_centers[comm_id]
+                
+                # 根据节点类型确定分布层
+                if node['type'] == 'D':
+                    # D类型节点紧密分布在社团中心附近
+                    r = D_LAYER_RADIUS * np.random.rand()
+                    theta = np.random.uniform(0, 2*np.pi)
+                    phi = np.random.uniform(0, np.pi)
+                    
+                    x = base_x + r * np.sin(phi) * np.cos(theta)
+                    y = base_y + r * np.sin(phi) * np.sin(theta)
+                    z = base_z + r * np.cos(phi)
+                else:
+                    # I/S类型节点分布在更大半径的球面上
+                    r = I_S_LAYER_RADIUS
+                    theta = np.random.uniform(0, 2*np.pi)
+                    phi = np.random.uniform(0, np.pi)
+                    
+                    x = base_x + r * np.sin(phi) * np.cos(theta)
+                    y = base_y + r * np.sin(phi) * np.sin(theta)
+                    z = base_z + r * np.cos(phi)
+                
+                coords[node_id] = (x, y, z)
+            
+            return coords
+        
+        def optimize_overlap(coords, iterations=100):
+            """ 使用斥力优化减少节点重叠 """
+            nodes = list(coords.keys())
+            positions = np.array(list(coords.values()))
+            
+            # 设置优化参数
+            repulsion = 0.1  # 斥力强度
+            min_dist = 0.5   # 最小间距
+            
+            for _ in range(iterations):
+                # 计算所有节点间距
+                diffs = positions[:, np.newaxis, :] - positions[np.newaxis, :, :]
+                dists = np.linalg.norm(diffs, axis=2)
+                
+                # 避免自比较
+                np.fill_diagonal(dists, np.inf)
+                
+                # 计算斥力
+                with np.errstate(divide='ignore'):
+                    forces = repulsion / (dists**2)
+                forces[dists > min_dist] = 0
+                
+                # 更新位置
+                movement = np.sum(forces[:, :, np.newaxis] * diffs, axis=1)
+                positions += movement
+                
+            # 归一化到0-10范围
+            scaler = MinMaxScaler(feature_range=(0, 10))
+            positions = scaler.fit_transform(positions)
+            
+            return {node: tuple(pos) for node, pos in zip(nodes, positions)}
+        '''结束定义'''
+        # 生成初始坐标
+        initial_coords = generate_3d_coordinates(nodeJson, communities)
+
+        # 优化防止重叠
+        final_coords = optimize_overlap(initial_coords)
+
+
+        '''结果示例输出,并用三维视图显示START'''
+        # for node_id, (x, y, z) in final_coords.items():
+        #     print(f"Node {node_id}: ({x:.2f}, {y:.2f}, {z:.2f})")
+
+        # import matplotlib.pyplot as plt
+        # from mpl_toolkits.mplot3d import Axes3D
+
+        # fig = plt.figure(figsize=(10, 8))
+        # ax = fig.add_subplot(111, projection='3d')
+
+        # # 按类型绘制节点
+        # types = {n['id']: n['type'] for n in nodeJson}
+        # colors = {'D': 'red', 'I': 'green', 'S': 'blue'}
+
+        # for node_id, (x, y, z) in final_coords.items():
+        #     ax.scatter(x, y, z, 
+        #             c=colors[types[node_id]],
+        #             s=50 if types[node_id] == 'D' else 30,
+        #             marker='o' if types[node_id] == 'D' else '^')
+
+        # # 绘制边
+        # for edge in edgeJson:
+        #     x = [final_coords[edge['from']][0], final_coords[edge['to']][0]]
+        #     y = [final_coords[edge['from']][1], final_coords[edge['to']][1]]
+        #     z = [final_coords[edge['from']][2], final_coords[edge['to']][2]]
+        #     ax.plot(x, y, z, c='gray', alpha=0.3)
+
+        # ax.set_xlabel('X')
+        # ax.set_ylabel('Y')
+        # ax.set_zlabel('Z')
+        # plt.show()
+        '''结果示例输出,并用三维视图显示END'''
+
+
+        '''END'''
+        # 将坐标添加到每个节点字典
+        for node in nodeJson:
+            node_id = node['id']
+            x, y, z = final_coords[node_id]
+            # 添加三维坐标字段
+            node['coordinates'] = {
+                'x': round(x, 4),  # 保留4位小数
+                'y': round(y, 4),
+                'z': round(z, 4)
+            }
+
+        return self.create(
+            result=result,
+            user=result.user,
+            nodes=nodeJson,
+            edges=edgeJson,
+            type=result.plan.algorithm.type,
+        )
+
+class Graph(models.Model):
+    create_time = models.DateTimeField(auto_now_add=True)
+    update_time = models.DateTimeField(auto_now=True)
+
+    type = models.CharField(choices=graphForAlgo, default='optimize', max_length=16)
+    # 根据算法不同,生成的图数据结构不同
+    nodes = models.JSONField()
+    edges = models.JSONField()
+
+    result = models.ForeignKey(to="api.Result", on_delete=models.CASCADE, related_name="own_graphs")
+    user = models.ForeignKey(to="api.User", on_delete=models.CASCADE, related_name="user_own_graphs")
+
+
+    objects = GraphManager()
+
+    def generateToken(self):
+        # 删除旧验证码
+        if self.own_tokens.exists():
+            for token in self.own_tokens.all():
+                token.delete()
+        # 生成验证码
+        token = GraphToken()
+        token.graph = self
+        token.token = ''.join([str(randint(0,9)) for n in range(6)])
+        while(GraphToken.objects.checkDuplicate(token.token)):
+            token.token = ''.join([str(randint(0,9)) for n in range(6)])
+        token.save()
+        return token.token
+    
+
+    class Meta:
+        app_label = 'api'
+
+

+ 3 - 0
backend/api/urls.py

@@ -5,6 +5,7 @@ from .api_prepare import UploadFileAPI, PlanAPI
 from .api_calculate import CalculateAPI
 from .api_rawDataTrans import RawDataTrans
 from .api_results import Results
+from .api_graph import GenerateGraph, ViewGraphByToken
 
 urlpatterns = [
     path('register/', UserRegisterAPI.as_view(), name='user_register_api'),
@@ -15,4 +16,6 @@ urlpatterns = [
     path('calculate/', CalculateAPI.as_view(), name='calculate_api'),
     path('rawDataTrans/', RawDataTrans.as_view(), name='rawDataTrans_api'),
     path('results/', Results.as_view(), name='results_api'),
+    path('generateGraph/', GenerateGraph.as_view(), name="generate_graph_api"),
+    path('viewGraphByToken/', ViewGraphByToken.as_view(), name="view_graph_by_token_api"),
 ]

BIN
backend/db.sqlite3


+ 0 - 0
scheduler/process_logs/proc_20250327-220139_13732.stderr


+ 7 - 0
scheduler/process_logs/proc_20250327-220139_13732.stdout

@@ -0,0 +1,7 @@
+THIS is a algo program
+data is
+{'edges': [{'from': '1', 'meta': [], 'to': '3'}, {'from': '2', 'meta': [], 'to': '1'}, {'from': '2', 'meta': [], 'to': '4'}, {'from': '2', 'meta': [], 'to': '3'}], 'nodes': [{'id': '1', 'meta': [], 'type': 'S'}, {'id': '2', 'meta': [], 'type': 'D'}, {'id': '3', 'meta': [], 'type': 'S'}, {'id': '4', 'meta': [], 'type': 'I'}]}
+[{'id': '1', 'meta': [], 'type': 'S'}, {'id': '2', 'meta': [], 'type': 'D'}, {'id': '3', 'meta': [], 'type': 'S'}, {'id': '4', 'meta': [], 'type': 'I'}]
+[{'from': '1', 'meta': [], 'to': '3'}, {'from': '2', 'meta': [], 'to': '1'}, {'from': '2', 'meta': [], 'to': '4'}, {'from': '2', 'meta': [], 'to': '3'}]
+response is ok
+算法控制程序推送结果完毕 MissionId: 51 PlanId: 173 Message: {'status': 'success', 'message': '保存结果文件成功', 'data': None}

+ 0 - 0
scheduler/process_logs/proc_20250327-220209_13732.stderr


+ 7 - 0
scheduler/process_logs/proc_20250327-220209_13732.stdout

@@ -0,0 +1,7 @@
+THIS is a algo program
+data is
+{'edges': [[1, 2], [1, 4], [2, 4], [3, 4]], 'nodes': [[1, 'S'], [2, 'D'], [3, 'D'], [4, 'I']]}
+[[1, 'S'], [2, 'D'], [3, 'D'], [4, 'I']]
+[[1, 2], [1, 4], [2, 4], [3, 4]]
+response is ok
+算法控制程序推送结果完毕 MissionId: 51 PlanId: 174 Message: {'status': 'success', 'message': '保存结果文件成功', 'data': None}

+ 0 - 0
scheduler/process_logs/proc_20250401-001805_21488.stderr


+ 7 - 0
scheduler/process_logs/proc_20250401-001805_21488.stdout

@@ -0,0 +1,7 @@
+THIS is a algo program
+data is
+{'edges': [{'from': '1', 'meta': [{'optimize': 'old'}], 'to': '3'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '1'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '4'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '3'}], 'nodes': [{'id': '1', 'meta': [{'optimize': 'old'}, {'group': '1'}], 'type': 'S'}, {'id': '2', 'meta': [{'optimize': 'old'}, {'group': '5'}], 'type': 'D'}, {'id': '3', 'meta': [{'optimize': 'old'}, {'group': '3'}], 'type': 'S'}, {'id': '4', 'meta': [{'optimize': 'old'}, {'group': '2'}], 'type': 'I'}]}
+[{'id': '1', 'meta': [{'optimize': 'old'}, {'group': '1'}], 'type': 'S'}, {'id': '2', 'meta': [{'optimize': 'old'}, {'group': '5'}], 'type': 'D'}, {'id': '3', 'meta': [{'optimize': 'old'}, {'group': '3'}], 'type': 'S'}, {'id': '4', 'meta': [{'optimize': 'old'}, {'group': '2'}], 'type': 'I'}]
+[{'from': '1', 'meta': [{'optimize': 'old'}], 'to': '3'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '1'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '4'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '3'}]
+response is ok
+算法控制程序推送结果完毕 MissionId: 57 PlanId: 195 Message: {'status': 'success', 'message': '保存结果文件成功', 'data': None}

+ 0 - 0
scheduler/process_logs/proc_20250401-001834_21488.stderr


+ 7 - 0
scheduler/process_logs/proc_20250401-001834_21488.stdout

@@ -0,0 +1,7 @@
+THIS is a algo program
+data is
+{'edges': [[1, 2], [1, 4], [2, 4], [3, 4]], 'nodes': [[1, 'S'], [2, 'D'], [3, 'D'], [4, 'I']]}
+[[1, 'S'], [2, 'D'], [3, 'D'], [4, 'I']]
+[[1, 2], [1, 4], [2, 4], [3, 4]]
+response is ok
+算法控制程序推送结果完毕 MissionId: 57 PlanId: 196 Message: {'status': 'success', 'message': '保存结果文件成功', 'data': None}

+ 0 - 0
scheduler/process_logs/proc_20250401-004055_21488.stderr


+ 7 - 0
scheduler/process_logs/proc_20250401-004055_21488.stdout

@@ -0,0 +1,7 @@
+THIS is a algo program
+data is
+{'edges': [{'from': '1', 'meta': [{'optimize': 'old'}], 'to': '3'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '1'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '4'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '3'}], 'nodes': [{'id': '1', 'meta': [{'optimize': 'old'}, {'group': '3'}], 'type': 'S'}, {'id': '2', 'meta': [{'optimize': 'old'}, {'group': '3'}], 'type': 'D'}, {'id': '3', 'meta': [{'optimize': 'old'}, {'group': '2'}], 'type': 'S'}, {'id': '4', 'meta': [{'optimize': 'old'}, {'group': '5'}], 'type': 'I'}]}
+[{'id': '1', 'meta': [{'optimize': 'old'}, {'group': '3'}], 'type': 'S'}, {'id': '2', 'meta': [{'optimize': 'old'}, {'group': '3'}], 'type': 'D'}, {'id': '3', 'meta': [{'optimize': 'old'}, {'group': '2'}], 'type': 'S'}, {'id': '4', 'meta': [{'optimize': 'old'}, {'group': '5'}], 'type': 'I'}]
+[{'from': '1', 'meta': [{'optimize': 'old'}], 'to': '3'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '1'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '4'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '3'}]
+response is ok
+算法控制程序推送结果完毕 MissionId: 58 PlanId: 198 Message: {'status': 'success', 'message': '保存结果文件成功', 'data': None}

+ 0 - 0
scheduler/process_logs/proc_20250401-004125_21488.stderr


+ 7 - 0
scheduler/process_logs/proc_20250401-004125_21488.stdout

@@ -0,0 +1,7 @@
+THIS is a algo program
+data is
+{'edges': [[1, 2], [1, 4], [2, 4], [3, 4]], 'nodes': [[1, 'S'], [2, 'D'], [3, 'D'], [4, 'I']]}
+[[1, 'S'], [2, 'D'], [3, 'D'], [4, 'I']]
+[[1, 2], [1, 4], [2, 4], [3, 4]]
+response is ok
+算法控制程序推送结果完毕 MissionId: 58 PlanId: 199 Message: {'status': 'success', 'message': '保存结果文件成功', 'data': None}

+ 0 - 0
scheduler/process_logs/proc_20250401-004232_21488.stderr


+ 7 - 0
scheduler/process_logs/proc_20250401-004232_21488.stdout

@@ -0,0 +1,7 @@
+THIS is a algo program
+data is
+{'edges': [{'from': '1', 'meta': [{'optimize': 'old'}], 'to': '3'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '1'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '4'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '3'}], 'nodes': [{'id': '1', 'meta': [{'optimize': 'old'}, {'group': '4'}], 'type': 'S'}, {'id': '2', 'meta': [{'optimize': 'old'}, {'group': '5'}], 'type': 'D'}, {'id': '3', 'meta': [{'optimize': 'old'}, {'group': '4'}], 'type': 'S'}, {'id': '4', 'meta': [{'optimize': 'old'}, {'group': '5'}], 'type': 'I'}]}
+[{'id': '1', 'meta': [{'optimize': 'old'}, {'group': '4'}], 'type': 'S'}, {'id': '2', 'meta': [{'optimize': 'old'}, {'group': '5'}], 'type': 'D'}, {'id': '3', 'meta': [{'optimize': 'old'}, {'group': '4'}], 'type': 'S'}, {'id': '4', 'meta': [{'optimize': 'old'}, {'group': '5'}], 'type': 'I'}]
+[{'from': '1', 'meta': [{'optimize': 'old'}], 'to': '3'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '1'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '4'}, {'from': '2', 'meta': [{'optimize': 'old'}], 'to': '3'}]
+response is ok
+算法控制程序推送结果完毕 MissionId: 59 PlanId: 201 Message: {'status': 'success', 'message': '保存结果文件成功', 'data': None}

+ 0 - 0
scheduler/process_logs/proc_20250401-004302_21488.stderr


+ 7 - 0
scheduler/process_logs/proc_20250401-004302_21488.stdout

@@ -0,0 +1,7 @@
+THIS is a algo program
+data is
+{'edges': [[1, 2], [1, 4], [2, 4], [3, 4]], 'nodes': [[1, 'S'], [2, 'D'], [3, 'D'], [4, 'I']]}
+[[1, 'S'], [2, 'D'], [3, 'D'], [4, 'I']]
+[[1, 2], [1, 4], [2, 4], [3, 4]]
+response is ok
+算法控制程序推送结果完毕 MissionId: 59 PlanId: 202 Message: {'status': 'success', 'message': '保存结果文件成功', 'data': None}

+ 3 - 0
viewer/package.json

@@ -15,8 +15,11 @@
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
     "axios": "^1.7.9",
+    "echarts": "^5.6.0",
     "element-plus": "^2.9.3",
     "sass-embedded": "^1.83.4",
+    "three": "^0.175.0",
+    "troisjs": "^0.3.4",
     "vue": "^3.5.13",
     "vue-router": "^4.5.0"
   },

+ 39 - 0
viewer/pnpm-lock.yaml

@@ -14,12 +14,21 @@ importers:
       axios:
         specifier: ^1.7.9
         version: 1.7.9
+      echarts:
+        specifier: ^5.6.0
+        version: 5.6.0
       element-plus:
         specifier: ^2.9.3
         version: 2.9.3(vue@3.5.13(typescript@5.7.3))
       sass-embedded:
         specifier: ^1.83.4
         version: 1.83.4
+      three:
+        specifier: ^0.175.0
+        version: 0.175.0
+      troisjs:
+        specifier: ^0.3.4
+        version: 0.3.4
       vue:
         specifier: ^3.5.13
         version: 3.5.13(typescript@5.7.3)
@@ -943,6 +952,9 @@ packages:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
 
+  echarts@5.6.0:
+    resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==, tarball: https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz}
+
   electron-to-chromium@1.5.92:
     resolution: {integrity: sha512-BeHgmNobs05N1HMmMZ7YIuHfYBGlq/UmvlsTgg+fsbFs9xVMj+xJHFg19GN04+9Q+r8Xnh9LXqaYIyEWElnNgQ==}
 
@@ -1712,6 +1724,9 @@ packages:
     resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
     engines: {node: ^14.18.0 || >=16.0.0}
 
+  three@0.175.0:
+    resolution: {integrity: sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==, tarball: https://registry.npmmirror.com/three/-/three-0.175.0.tgz}
+
   to-regex-range@5.0.1:
     resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
     engines: {node: '>=8.0'}
@@ -1720,12 +1735,18 @@ packages:
     resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
     engines: {node: '>=6'}
 
+  troisjs@0.3.4:
+    resolution: {integrity: sha512-aonG2lw+QyTjytJpvYIaGv2AiIKzm+eSYT4ByG64uVh+jCLAWxnblS6bCewIckvCMqs2j3z351Q/IFIPWYViCQ==, tarball: https://registry.npmmirror.com/troisjs/-/troisjs-0.3.4.tgz}
+
   ts-api-utils@2.0.1:
     resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==}
     engines: {node: '>=18.12'}
     peerDependencies:
       typescript: '>=4.8.4'
 
+  tslib@2.3.0:
+    resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==, tarball: https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz}
+
   tslib@2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
@@ -1909,6 +1930,9 @@ packages:
     resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
     engines: {node: '>=18'}
 
+  zrender@5.6.1:
+    resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==, tarball: https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz}
+
 snapshots:
 
   '@ampproject/remapping@2.3.0':
@@ -2775,6 +2799,11 @@ snapshots:
 
   delayed-stream@1.0.0: {}
 
+  echarts@5.6.0:
+    dependencies:
+      tslib: 2.3.0
+      zrender: 5.6.1
+
   electron-to-chromium@1.5.92: {}
 
   element-plus@2.9.3(vue@3.5.13(typescript@5.7.3)):
@@ -3494,16 +3523,22 @@ snapshots:
       '@pkgr/core': 0.1.1
       tslib: 2.8.1
 
+  three@0.175.0: {}
+
   to-regex-range@5.0.1:
     dependencies:
       is-number: 7.0.0
 
   totalist@3.0.1: {}
 
+  troisjs@0.3.4: {}
+
   ts-api-utils@2.0.1(typescript@5.7.3):
     dependencies:
       typescript: 5.7.3
 
+  tslib@2.3.0: {}
+
   tslib@2.8.1: {}
 
   type-check@0.4.0:
@@ -3663,3 +3698,7 @@ snapshots:
   yocto-queue@0.1.0: {}
 
   yoctocolors@2.1.1: {}
+
+  zrender@5.6.1:
+    dependencies:
+      tslib: 2.3.0

+ 28 - 0
viewer/src/views/dashoard/VRView.vue

@@ -0,0 +1,28 @@
+<template>
+    <div>VR
+        {{props.result}}
+    </div>
+</template>
+<script setup>
+import { onMounted, ref } from 'vue'
+import { postData, getData } from '@/api/axios.js'
+import { ElMessage } from 'element-plus'
+const props = defineProps({
+    result: {
+        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>

+ 159 - 26
viewer/src/views/dashoard/calculate.vue

@@ -28,19 +28,75 @@
             :style="{
               transform: `translate(${plan.position.x}px, ${plan.position.y}px)`
             }">
-            <el-button class="plan-button" :style="progressStyle(index)" @click="simulateProgress(index)">
+            <el-button class="plan-button" :style="progressStyle(index)" @click="viewResult(index)">
               {{ plan.algorithm.label }}
             </el-button>
           </div>
         </div>
       </div>
     </div>
+    <!-- 用于展示结果视图的modal框 -->
+    <el-dialog v-model="showModal" title="选择展示方式" :close-on-click-modal="false" :show-close="false" width="80%">
+      <!-- 操作按钮区域 -->
+      <div class="mode-buttons">
+        <el-button type="primary" @click="switchView('chart')">
+          <el-icon>
+            <PieChart />
+          </el-icon>
+          以图表视图查看
+        </el-button>
+
+        <el-button type="success" @click="switchView('3d')">
+          <el-icon>
+            <Promotion />
+          </el-icon>
+          以3D视图查看
+        </el-button>
+
+        <el-button type="warning" @click="switchView('vr')">
+          <el-icon>
+            <VideoCamera />
+          </el-icon>
+          以VR视图查看
+        </el-button>
+      </div>
+      <!-- 动态内容区域 -->
+      <div class="content-area">
+        <template v-if="currentView">
+          <component :is="currentViewComponent" :result="currentResult"/>
+        </template>
+        <el-empty
+          v-else
+          description="请选择视图展示方式"
+          class="empty-tip"
+        >
+          <template #image>
+            <el-icon :size="80" color="var(--el-color-info)">
+              <Select />
+            </el-icon>
+          </template>
+          <div class="tip-text">
+            点击上方按钮选择查看方式
+          </div>
+        </el-empty>
+      </div>
+
+      <template #footer>
+        <el-button @click="closeModal">关闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 <script setup>
-import { inject, onMounted, ref, computed } from 'vue'
+import { inject, onMounted, ref, computed, defineAsyncComponent, shallowRef } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { postData, getData } from '@/api/axios.js'
+import {
+  PieChart,
+  Promotion,
+  VideoCamera,
+  Select,
+} from '@element-plus/icons-vue'
 
 const useAnalyzeInfo = inject('analyzeInfo')
 const plans = inject('plans')
@@ -51,7 +107,25 @@ const connections = ref([])
 const flowContainer = ref(null)
 const buttonRefs = ref(null)
 const progressInterval = ref(null)
-// console.log(plans.value)
+
+// 视图展示控制变量
+// 加载组件
+const ChartView = defineAsyncComponent(() => import('./chartView.vue'))
+const ThreeDView = defineAsyncComponent(() => import('./threeDView.vue'))
+const VRView = defineAsyncComponent(() => import('./VRView.vue'))
+
+// 状态管理
+const showModal = ref(false)
+const currentView = ref(null)
+const currentResult = ref(null)
+
+// 动态组件处理
+const currentViewComponent = shallowRef(null)
+const viewComponents = {
+  'chart': ChartView,
+  '3d': ThreeDView,
+  'vr': VRView,
+}
 
 // 控制按钮内部进度条
 // 动态样式计算
@@ -87,17 +161,36 @@ const generateGradient = (value, color) => {
   )`
 }
 
-// 进度模拟
-const simulateProgress = (index) => {
-  progress.value[index] = 0
-  if (progress.value[index] >= 100) return
+// 查看结果
+const viewResult = (index) => {
+  if (progress.value[index] == 100) {
+    // 初始化视图显示框
+    currentView.value = null
+    currentViewComponent.value = null
+    currentResult.value = null
+    currentResult.value = index
+    showModal.value = true
+    
+  } else {
+    ElMessage.warning("请先开始计算处理,或等待该任务完成")
+  }
 
-  const interval = setInterval(() => {
-    progress.value[index] += 2
-    if (progress.value[index] >= 100) {
-      clearInterval(interval)
-    }
-  }, 100)
+}
+
+// 选择视图显示模式
+const switchView = (type) => {
+  currentView.value = type
+  currentViewComponent.value = viewComponents[type]
+
+}
+
+// 关闭视图显示框
+const closeModal = () => {
+  showModal.value = false
+  // 清空当前视图
+  currentView.value = null
+  currentResult.value = null
+  currentViewComponent.value = null
 }
 
 
@@ -169,36 +262,42 @@ const createSmartPath = (start, end) => {
 }
 
 const startCalculate = async () => {
+  // 测试用模拟进度100%
+  progress.value.forEach((item, index) => progress.value[index] = 100)
+  return
+
+  
+
   const response = await postData('/calculate/', {
     mission: mission.id,
     command: 'start',
   })
   progressInterval.value = setInterval(async () => {
-    try{
-      const response = await getData('/results', {'mission': JSON.stringify(mission)})
+    try {
+      const response = await getData('/results', { 'mission': JSON.stringify(mission) })
       // 成功获取进度更新信息,将进度赋值给progress
       console.log(response)
-      if(response.status === "success"){
+      if (response.status === "success") {
         let missionComplete = true
         response.data.forEach(result => {
           // 查找序号,根据序号修改progress
           const index = plans.value.findIndex(p => p.id === result.planId)
           progress.value[index] = result.progress
           // 只要有一个没有全部完成的任务,就不会停止获取进度
-          if(result.progress !== 100){
+          if (result.progress !== 100) {
             missionComplete = false
-            }
+          }
         })
-        if(missionComplete){
-          ElMessageBox.alert("任务已全部完成" ,"", {
+        if (missionComplete) {
+          ElMessageBox.alert("任务已全部完成", "", {
             confirmButtonText: '确定',
           })
           clearInterval(progressInterval.value)
         }
-      }else{
+      } else {
         ElMessage.error("更新任务信息失败,", response.message)
       }
-    }catch(error){
+    } catch (error) {
       console.log("catch error when start interval", error)
       clearInterval(progressInterval.value)
     }
@@ -208,9 +307,9 @@ const startCalculate = async () => {
 const pauseCalculate = () => {
   console.log('pause')
   //暂停、停止时都清除定时器
-  try{
+  try {
     clearInterval(progressInterval.value)
-  }catch(error){
+  } catch (error) {
     console.log(error)
   }
 }
@@ -218,9 +317,9 @@ const pauseCalculate = () => {
 const stopCalculate = () => {
   console.log('stop')
   //暂停、停止时都清除定时器
-  try{
+  try {
     clearInterval(progressInterval.value)
-  }catch(error){
+  } catch (error) {
     console.log(error)
   }
 }
@@ -379,4 +478,38 @@ onMounted(() => {
   display: flex;
   gap: 12px;
 }
+
+
+.mode-buttons {
+  margin-bottom: 20px;
+  display: flex;
+  gap: 15px;
+  justify-content: center;
+}
+
+.content-area {
+  min-height: 600px;
+  border: 1px solid var(--el-border-color);
+  border-radius: var(--el-border-radius-base);
+  position: relative;
+}
+
+.empty-tip {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 100%;
+}
+
+.tip-text {
+  color: var(--el-text-color-secondary);
+  margin-top: 10px;
+  font-size: 14px;
+}
+/* 按钮图标样式 */
+.el-button [class*=el-icon] + span {
+  margin-left: 6px;
+}
+
 </style>

+ 213 - 0
viewer/src/views/dashoard/chartView.vue

@@ -0,0 +1,213 @@
+<template>
+    <div class="dashboard-container">
+        <el-card shadow="hover" style="height: 600px; width: 100%; position: relative">
+            <!-- 加载状态 -->
+            <div v-loading="loading" element-loading-text="数据加载中..."
+                element-loading-background="rgba(255, 255, 255, 0.8)" style="height: 100%; width: 100%">
+
+                <!-- 数据展示区域 -->
+                <div v-show="!loading" class="chart-container" >
+                    <!-- 第一行:关键指标 -->
+                    <el-row :gutter="20" class="mb-20" >
+                        <el-col :span="8">
+                            <div class="chart-title">节点类型分布</div>
+                            <div ref="nodePieChart" style="height: 250px; width: 500px"></div>
+                        </el-col>
+                        <el-col :span="16">
+                            <div class="chart-title">节点度数分布</div>
+                            <div ref="degreeBarChart" style="height: 250px; width: 950px"></div>
+                        </el-col>
+                    </el-row>
+
+                    <!-- 第二行:链式分析 -->
+                    <el-row>
+                        <el-col :span="24">
+                            <div class="chart-title">链式结构分析</div>
+                        </el-col>
+                    </el-row>
+                    <el-row :gutter="20" class="mb-20">
+                        <el-col :span="4">
+                            <el-statistic title="总链数" :value="data.chains.num" style="width: 100%; padding-top: 40%; text-align: center;"/>
+                        </el-col>
+                        <el-col :span="4">
+                            <el-statistic title="平均长度" :value="data.chains.averageLength" style="width: 100%; padding-top: 40%; text-align: center;"/>
+                        </el-col>
+                        <el-col :span="16">
+                            <div ref="chainChart" style="width: 950px; height: 250px"></div>
+                        </el-col>
+                    </el-row>
+                </div>
+            </div>
+        </el-card>
+    </div>
+</template>
+<script setup>
+import { ref, onMounted, nextTick } from 'vue'
+import * as echarts from 'echarts'
+import { ElLoading } from 'element-plus'
+import { getData } from '@/api/axios.js'
+
+const props = defineProps({
+    result: {
+        type: Number,
+        required: true,
+    }
+})
+const loading = ref(true)
+const data = ref({
+    sNodesNum: 0,
+    dNodesNum: 0,
+    iNodesNum: 0,
+    sDegree: [],
+    dDegree: [],
+    iDegree: [],
+    chains: {
+        num: 0,
+        averageLength: 0,
+        numByLength: []
+    }
+})
+
+// DOM引用
+const nodePieChart = ref(null)
+const degreeBarChart = ref(null)
+const chainChart = ref(null)
+
+// 图表颜色配置
+const colorPalette = [
+    '#5470c6', '#91cc75', '#fac858',
+    '#ee6666', '#73c0de', '#3ba272',
+    '#fc8452', '#9a60b4', '#ea7ccc'
+]
+
+// 获取数据(模拟API调用)
+const fetchData = async () => {
+    try {
+        // 这里替换为实际的API调用
+        const response = await getData('/generateGraph', {method: 'chart', result: props.result})
+        data.value = response.data
+    } finally {
+        loading.value = false
+        nextTick()
+        initCharts()
+    }
+}
+
+// 初始化图表
+const initCharts = () => {
+    initNodePieChart()
+    initDegreeChart()
+    initChainChart()
+}
+
+// 节点类型饼图
+const initNodePieChart = () => {
+    const chart = echarts.init(nodePieChart.value)
+    const option = {
+        color: colorPalette,
+        tooltip: { trigger: 'item' },
+        legend: { top: 'bottom' },
+        series: [{
+            type: 'pie',
+            radius: ['40%', '70%'],
+            data: [
+                { value: data.value.sNodesNum, name: 'S节点' },
+                { value: data.value.dNodesNum, name: 'D节点' },
+                { value: data.value.iNodesNum, name: 'I节点' }
+            ],
+            label: { show: false },
+            emphasis: {
+                label: { show: true }
+            }
+        }],
+    }
+    chart.setOption(option)
+}
+
+// 度数分布柱状图
+const initDegreeChart = () => {
+    const chart = echarts.init(degreeBarChart.value)
+    const option = {
+        color: colorPalette,
+        tooltip: { trigger: 'axis' },
+        xAxis: {
+            type: 'category',
+            data: data.value.sDegree.map((_, i) => `度数 ${i}`)
+        },
+        yAxis: { type: 'value' },
+        legend: { data: ['S节点', 'D节点', 'I节点'] },
+        series: [
+            { name: 'S节点', type: 'bar', data: data.value.sDegree },
+            { name: 'D节点', type: 'bar', data: data.value.dDegree },
+            { name: 'I节点', type: 'bar', data: data.value.iDegree }
+        ]
+    }
+    chart.setOption(option)
+}
+
+// 链式结构分析
+const initChainChart = () => {
+    const chart = echarts.init(chainChart.value)
+
+    //横坐标,最后一个是长度大于等于5
+    const xAxisData = data.value.chains.numByLength.map((_, i) => {
+        if(i == data.value.chains.numByLength.length - 1){
+            return `长度 ${i + 1} 以上`
+        }else{
+            return `长度 ${i + 1}`
+        }
+    })
+
+    const option = {
+        color: colorPalette,
+        tooltip: { trigger: 'axis' },
+        xAxis: {
+            type: 'category',
+            data: xAxisData,
+        },
+        yAxis: { type: 'value' },
+        series: [{
+            type: 'bar',
+            data: data.value.chains.numByLength,
+            itemStyle: { borderRadius: 5 }
+        }]
+    }
+    chart.setOption(option)
+}
+
+
+onMounted(fetchData)
+</script>
+
+<style scoped>
+.dashboard-container {
+    padding: 20px;
+}
+
+.chart-title {
+    font-size: 16px;
+    font-weight: 600;
+    margin-bottom: 15px;
+    color: var(--el-text-color-primary);
+}
+
+.chain-stats {
+    display: flex;
+    gap: 40px;
+    align-items: center;
+    margin-bottom: 20px;
+    width: 100%;
+}
+
+.mb-20 {
+    margin-bottom: 20px;
+}
+
+/* 响应式调整 */
+@media (max-width: 1200px) {
+    .chain-stats {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+}
+</style>

+ 209 - 0
viewer/src/views/dashoard/threeDView.vue

@@ -0,0 +1,209 @@
+<template>
+    <div class="container">
+        <el-card class="full-width-card">
+            <div class="toolbar">
+                <p>三维图数据可视化</p>
+                <el-button @click="resetCamera">对正视角</el-button>
+            </div>
+            <div class="graph-container">
+                <Renderer ref="renderer" antialias orbit-ctrl resize :alpha="true" :background-alpha="0">
+                    <Camera :position="initialCameraPos" />
+                    <Scene ref="scene">
+                        <!-- 使用空组件的 ref 绑定场景 -->
+                        <Group ref="nodesGroup" />
+                    </Scene>
+                </Renderer>
+            </div>
+        </el-card>
+    </div>
+</template>
+<script setup>
+import { onMounted, ref, watch, nextTick, onBeforeUnmount } from 'vue';
+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';
+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 initialCameraPos = ref({ x: 0, y: 0, z: 1 })
+const initialCameraState = ref({
+    position: new THREE.Vector3(),
+    target: new THREE.Vector3()
+});
+const cameraCenter = ref({})
+const cameraDistance = ref(null)
+
+// 场景对象
+const scene = ref(null);
+const nodesGroup = ref(null);
+let sceneBoundingSphere = new THREE.Sphere(); // 场景包围球
+
+
+// 动态创建边的函数
+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 = [];
+
+    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 material = new THREE.LineBasicMaterial({ color: 0x800080 });
+
+        const line = new THREE.Line(geometry, material);
+        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 handleResize = () => {
+    nextTick(() => {
+        if (renderer.value?.renderer) {
+            const container = document.querySelector('.graph-container');
+            // 保持宽高比避免变形
+            renderer.value.camera.aspect = container.clientWidth / container.clientHeight;
+            renderer.value.camera.updateProjectionMatrix();
+            renderer.value.renderer.setSize(container.clientWidth, container.clientHeight);
+        }
+    });
+};
+
+
+// 修正重置函数
+const resetCamera = () => {
+    console.log(initialCameraState.value.position)
+
+    // 恢复到保存的初始状态
+    renderer.value.camera.position.copy(initialCameraState.value.position);
+    renderer.value.three.cameraCtrl.target.copy(initialCameraState.value.target);
+
+    // 必须执行以下操作
+    renderer.value.three.cameraCtrl.update();
+    renderer.value.three.cameraCtrl.reset(); // 调用原生重置方法
+    renderer.value.camera.lookAt(initialCameraState.value.target);
+};
+
+onMounted(() => {
+    window.addEventListener('resize', handleResize);
+    // 根据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])
+        })
+        nextTick(() => {
+            createEdges();
+            handleResize();
+
+            // 计算场景包围盒
+            const box = new THREE.Box3().setFromObject(nodesGroup.value.group);
+            const center = box.getCenter(new THREE.Vector3());
+            const size = box.getSize(new THREE.Vector3()).length();
+
+            // 保存初始参数
+            initialCameraState.value.position.set(
+                center.x,
+                center.y,
+                center.z + size * 1.5 // 初始距离
+            );
+            initialCameraState.value.target.copy(center);
+
+            // 强制更新控制器
+            renderer.value.three.cameraCtrl.target.copy(center);
+            renderer.value.three.cameraCtrl.update();
+            renderer.value.three.cameraCtrl.saveState(); // 关键:保存初始状态
+        });
+    }).catch(error => {
+        ElMessage.error("获取图数据失败")
+        console.log(error)
+    })
+})
+
+onBeforeUnmount(() => {
+    window.removeEventListener('resize', handleResize);
+});
+</script>
+
+
+<style scoped>
+.el-card {
+    height: 100%;
+}
+
+.graph-container {
+    height: 100%;
+}
+
+canvas {
+    margin: auto;
+    display: block;
+    background: transparent !important;
+    mix-blend-mode: normal !important;
+}
+
+.container {
+    height: 600px;
+    display: flex;
+    flex-direction: column;
+}
+
+.full-width-card {
+    flex: 1;
+    min-height: 0;
+}
+
+:deep(.el-card__body) {
+    height: 100%;
+    padding: 0px;
+}
+
+.toolbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0 20px;
+}
+</style>