Explorar o código

fix: 关系图谱右上角工具条合并,修复画布不铺满容器

- 移除第一排 icon 工具条,保留第二排画布工具条;把查找、过滤、扩散关系、框选合并到画布工具条,重复的布局/全屏操作只保留一份。- 增加 ResizeObserver 显式同步 G6 画布尺寸,修复聊天容器/全屏切换、缩放后画布留白不能铺满的问题;画布容器补 width:100%。
cc hai 2 días
pai
achega
c687b073cd

+ 73 - 60
ai-electron/frontend/src/ai/components/graph/GraphAnalysisViewer.vue

@@ -13,26 +13,6 @@
         </div>
 
         <div class="control-tools-box">
-          <div class="custom-atlas-tools-container">
-            <ul>
-              <Tooltip
-                v-for="item in analysisToolbarItems"
-                :key="item.key"
-                :title="item.tipsContent ? `${item.label}: ${item.tipsContent}` : item.label"
-              >
-                <li
-                  class="tool-item"
-                  :class="{ 'gap-item': item.gap, 'is-active': isToolbarItemActive(item) }"
-                  @click="handleToolbarClick(item)"
-                >
-                  <span class="tool-item-icon">
-                    <component :is="item.icon" class="icon-style" />
-                  </span>
-                </li>
-              </Tooltip>
-            </ul>
-          </div>
-
           <div v-if="activePanel" class="tool-panel">
             <div class="tool-panel__header">
               <div class="tool-panel__title">{{ activePanelLabel }}</div>
@@ -173,6 +153,27 @@
           </div>
 
           <div class="canvas-toolbar">
+            <Tooltip title="查找">
+              <Button
+                type="text"
+                class="canvas-toolbar__button"
+                :class="{ 'is-active': activePanel === 'find' }"
+                @click="togglePanel('find')"
+              >
+                <SearchOutlined />
+              </Button>
+            </Tooltip>
+            <Tooltip title="过滤">
+              <Button
+                type="text"
+                class="canvas-toolbar__button"
+                :class="{ 'is-active': activePanel === 'filter' }"
+                @click="togglePanel('filter')"
+              >
+                <FilterOutlined />
+              </Button>
+            </Tooltip>
+            <span class="canvas-toolbar__divider"></span>
             <Tooltip title="缩小">
               <Button type="text" class="canvas-toolbar__button" @click="zoomOut">
                 <MinusOutlined />
@@ -194,6 +195,16 @@
               </Button>
             </Tooltip>
             <span class="canvas-toolbar__divider"></span>
+            <Tooltip title="扩散关系">
+              <Button type="text" class="canvas-toolbar__button" @click="selectNeighborElements">
+                <ShareAltOutlined />
+              </Button>
+            </Tooltip>
+            <Tooltip title="框选">
+              <Button type="text" class="canvas-toolbar__button" @click="showBoxSelectTip">
+                <BorderOutlined />
+              </Button>
+            </Tooltip>
             <Tooltip title="重新布局">
               <Button type="text" class="canvas-toolbar__button" @click="relayout">
                 <ApartmentOutlined />
@@ -340,27 +351,9 @@
   const FILTER_OPERATORS = ['>', '>=', '=', '<', '<='] as const;
   const filterOperators = FILTER_OPERATORS;
 
-  interface AnalysisToolbarItem {
-    key: 'find' | 'filter' | 'selectNeighbor' | 'boxSelect' | 'forceLayout' | 'fullScreen';
-    label: string;
-    icon: any;
-    tipsContent?: string;
-    panelKey?: 'find' | 'filter';
-    gap?: boolean;
-  }
-
-  /** 只读版工具条:保留原页面的查找/过滤/扩散关系/框选/智能布局/全屏,去掉编辑类按钮 */
-  const analysisToolbarItems: AnalysisToolbarItem[] = [
-    { key: 'find', label: '查找', icon: SearchOutlined, panelKey: 'find' },
-    { key: 'filter', label: '过滤', icon: FilterOutlined, panelKey: 'filter' },
-    { key: 'selectNeighbor', label: '扩散关系', icon: ShareAltOutlined, tipsContent: '选中当前实体的相邻实体和关系' },
-    { key: 'boxSelect', label: '框选', icon: BorderOutlined, tipsContent: 'G6 当前通过 Shift 拖拽框选' },
-    { key: 'forceLayout', label: '智能布局', icon: ApartmentOutlined, tipsContent: '按对象关系分析的力导向重新布局' },
-    { key: 'fullScreen', label: '全屏预览', icon: FullscreenOutlined, gap: true },
-  ];
-
   const stageRef = ref<HTMLDivElement>();
   const graphRef = shallowRef<Graph | null>(null);
+  let stageResizeObserver: ResizeObserver | null = null;
   const building = ref(false);
   const zoomPercent = ref(100);
   const performanceMode = ref(false);
@@ -1008,28 +1001,12 @@
 
   /* ---------------- 工具条行为 ---------------- */
 
-  const isToolbarItemActive = (item: AnalysisToolbarItem) => item.panelKey === activePanel.value;
+  const togglePanel = (panel: 'find' | 'filter') => {
+    activePanel.value = activePanel.value === panel ? null : panel;
+  };
 
-  const handleToolbarClick = (item: AnalysisToolbarItem) => {
-    if (item.panelKey) {
-      activePanel.value = activePanel.value === item.panelKey ? null : item.panelKey;
-      return;
-    }
-    if (item.key === 'selectNeighbor') {
-      selectNeighborElements();
-      return;
-    }
-    if (item.key === 'boxSelect') {
-      showMessage('按住 Shift 拖动画布即可框选', 'info');
-      return;
-    }
-    if (item.key === 'forceLayout') {
-      void relayout();
-      return;
-    }
-    if (item.key === 'fullScreen') {
-      emit('toggle-fullscreen');
-    }
+  const showBoxSelectTip = () => {
+    showMessage('按住 Shift 拖动画布即可框选', 'info');
   };
 
   const selectNeighborElements = () => {
@@ -1085,6 +1062,8 @@
   }
 
   function destroyGraph() {
+    stageResizeObserver?.disconnect();
+    stageResizeObserver = null;
     const graph = graphRef.value;
     graphRef.value = null;
     try {
@@ -1094,6 +1073,28 @@
     }
   }
 
+  /**
+   * G6 自带 autoResize 在聊天流式布局/全屏切换时偶尔拿不到最终尺寸,
+   * 这里再用 ResizeObserver 显式 resize,保证画布始终铺满容器,缩放后不留白。
+   */
+  function observeStageSize() {
+    if (typeof ResizeObserver === 'undefined' || !stageRef.value) return;
+    stageResizeObserver?.disconnect();
+    stageResizeObserver = new ResizeObserver(() => {
+      const element = stageRef.value;
+      const graph = graphRef.value;
+      if (!element || !graph) return;
+      const width = Math.max(1, element.clientWidth);
+      const height = Math.max(1, element.clientHeight);
+      try {
+        graph.resize(width, height);
+      } catch {
+        /* 忽略尺寸同步异常 */
+      }
+    });
+    stageResizeObserver.observe(stageRef.value);
+  }
+
   function createNodeOptions() {
     return {
       type: performanceMode.value ? 'ai-graph-performance-node' : 'ai-graph-node',
@@ -1254,6 +1255,7 @@
       graphRef.value = graph;
       bindGraphEvents(graph);
       await graph.render();
+      observeStageSize();
       await graph.fitView({ when: 'always' }, false);
       refreshZoom();
     } catch (error) {
@@ -1704,6 +1706,7 @@
   .ai-graph-viewer {
     display: flex;
     flex-direction: column;
+    width: 100%;
     height: 100%;
     min-height: 0;
     background: var(--ai-bg, #fff);
@@ -1933,12 +1936,14 @@
 
   .relation-stage {
     flex: 1 1 auto;
+    width: 100%;
     min-height: 0;
     overflow: hidden;
   }
 
   .relation-stage__canvas {
     position: relative;
+    width: 100%;
     height: 100%;
     min-height: 0;
     overflow: hidden;
@@ -2220,7 +2225,10 @@
     z-index: 1;
     display: flex;
     align-items: center;
+    justify-content: flex-end;
+    flex-wrap: wrap;
     gap: 4px;
+    max-width: calc(100vw - 48px);
     padding: 7px;
     border-radius: 16px;
     border: 1px solid rgba(148, 163, 184, 0.2);
@@ -2243,6 +2251,11 @@
       color: #2563eb;
       background: rgba(59, 130, 246, 0.1);
     }
+
+    &.is-active {
+      color: #2563eb;
+      background: rgba(59, 130, 246, 0.12);
+    }
   }
 
   .canvas-toolbar__divider {