Răsfoiți Sursa

Merge branch 'master' into fix-subapp-loading

hongrunxia 1 săptămână în urmă
părinte
comite
9698e3f8e4

+ 1 - 1
src/views/vent/monitorManager/deviceMonitor/components/device/index.vue

@@ -32,7 +32,7 @@
       <span class="location-text">定位图标显示</span>
     </div>
     <div
-      v-if="deviceType.startsWith('windrect')"
+      v-if="false && deviceType.startsWith('windrect')"
       class="topology-icon"
       :class="{ 'topology-btn-show': !topologyShow, 'topology-btn-hide': topologyShow }"
       @click="showTree('windTopology', true)"

+ 6 - 1
src/views/vent/monitorManager/windrectMonitor/topology/index.vue

@@ -31,7 +31,7 @@
     </a-row>
 
     <!-- 新增/编辑测风地点弹窗 -->
-    <BasicModal @register="registerModal" :width="600" :min-height="500" centered :title="modalTitle" @ok="handleSubmit" destroy-on-close>
+    <BasicModal @register="registerModal" :width="600" :min-height="420" centered :title="modalTitle" @ok="handleSubmit" destroy-on-close>
       <BasicForm @register="registerForm" />
     </BasicModal>
   </div>
@@ -170,6 +170,11 @@
       showActionColumn: false,
       useSearchForm: false,
       showTableSetting: false,
+      pagination: false,
+      scroll: {
+        x: 'max-content',
+        y: 700,
+      },
       // 与 windrectMonitor/components/modalTable.vue 一致:合并 readData 子对象
       afterFetch: (list) => (list || []).map((d: Recordable) => Object.assign({}, d, (d as any).readData)),
     },

+ 2 - 2
src/views/vent/monitorManager/windrectMonitor/topology/windTopology.data.ts

@@ -17,7 +17,7 @@ export const categories: CategoryDef[] = [
 
 /** 测风地点统一节点样式:浅灰圆点(层级/类型区分转移到连线颜色;浅色在深色拓扑底上更清晰) */
 export const POINT_COLOR = '#c8d3de';
-export const POINT_SIZE = 16;
+export const POINT_SIZE = 10;
 
 // ==================== 测风地点层级映射 ====================
 
@@ -59,7 +59,7 @@ export const LAYOUT = {
   /** 层级列间距(x 轴间隔,固定) */
   colGap: 260,
   /** 保存布局时坐标对齐网格大小(px):保存时 topologyX/topologyY 按此取整 */
-  snapGrid: 50,
+  snapGrid: 10,
   /** 各层级(y 轴)节点间距,未配置的层级回退 rowGapDefault */
   rowGap: { 0: 300, 1: 300, 2: 300, 3: 200, 4: 300, 5: 300, 6: 300, ungraded: 200 } as Record<number, number> & { ungraded: number },
   /** 未在 rowGap 中配置的层级使用的节点间距(px) */

+ 21 - 0
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -67,6 +67,7 @@
             @ask-user-submit="handleAskUserSubmit"
             @ask-user-cancel="handleAskUserCancel"
             @agent-detail-click="handleAgentDetailClick"
+            @open-markdown-tab="openMarkdownPreviewTab"
           />
 
           <!-- 任务进度条 -->
@@ -117,6 +118,7 @@
             @schedule-deleted="handleScheduleDeleted"
             @schedule-updated="handleScheduleUpdated"
             @open-schedule-session="handleOpenScheduleSession"
+            @open-markdown-tab="openMarkdownPreviewTab"
           />
         </Transition>
       </template>
@@ -1070,6 +1072,25 @@
     });
   };
 
+  // 在右侧面板新建标签页展示 Markdown 代码块渲染效果(相同内容复用同一标签)
+  const openMarkdownPreviewTab = (payload: { title: string; content: string }) => {
+    let hash = 0;
+    for (let i = 0; i < payload.content.length; i++) {
+      hash = (hash * 31 + payload.content.charCodeAt(i)) | 0;
+    }
+    openFileTab(
+      {
+        id: `md-preview-${hash}`,
+        name: `${payload.title}.md`,
+        size: payload.content.length,
+        type: 'text/markdown',
+        content: payload.content,
+        uploadTime: '',
+      },
+      false
+    );
+  };
+
   const openFilePreview = (file: AttachedFile) => {
     openFileTab(file, false);
 

+ 3 - 117
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -301,19 +301,6 @@
     <div v-if="!isAtBottom" class="scroll-to-bottom" @click="scrollToBottom">
       <div class="scroll-to-bottom-icon"></div>
     </div>
-
-    <!-- 表格预览弹窗 -->
-    <div v-if="showPreviewModal" class="table-preview-overlay" @click.self="closePreview">
-      <div class="table-preview-modal">
-        <div class="table-preview-header">
-          <span class="table-preview-title">表格预览</span>
-          <div class="table-preview-close" @click="closePreview">
-            <img :src="getCssUrl('--img-chat-close-icon-btn')" width="16" height="16" />
-          </div>
-        </div>
-        <div class="table-preview-body" v-html="previewTableHtml"></div>
-      </div>
-    </div>
   </div>
 </template>
 
@@ -437,6 +424,7 @@
     (e: 'ask-user-submit', answers: string[]): void;
     (e: 'ask-user-cancel'): void;
     (e: 'agent-detail-click', data: AgentDetailData): void;
+    (e: 'open-markdown-tab', payload: { title: string; content: string }): void;
   }>();
 
   const isEditableFile = (filename: string) => isMdFile(filename) || isTextFile(filename);
@@ -608,10 +596,6 @@
   const isAtBottom = ref(true);
   const copiedMessageIndex = ref(-1);
 
-  // 表格预览状态
-  const previewTableHtml = ref('');
-  const showPreviewModal = ref(false);
-
   const handleTableAction = (e: Event) => {
     const target = (e.target as HTMLElement).closest('.table-action-btn') as HTMLElement;
     if (!target) return;
@@ -670,18 +654,13 @@
         break;
       }
       case 'preview': {
-        previewTableHtml.value = tableHtml;
-        showPreviewModal.value = true;
+        // 在右侧面板新建标签页展示表格渲染效果
+        emit('open-markdown-tab', { title: '表格预览', content: tableHtmlToMarkdown(tableHtml) });
         break;
       }
     }
   };
 
-  const closePreview = () => {
-    showPreviewModal.value = false;
-    previewTableHtml.value = '';
-  };
-
   const checkScrollBottom = () => {
     const el = messagesRef.value;
     if (!el) return;
@@ -1859,99 +1838,6 @@
     }
   }
 
-  /* 表格预览弹窗 */
-  .table-preview-overlay {
-    position: fixed;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    background: rgba(0, 0, 0, 0.6);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    z-index: 1000;
-  }
-
-  .table-preview-modal {
-    background: #1a1f2e;
-    border: 1px solid rgba(63, 80, 106, 0.5);
-    border-radius: 8px;
-    width: 90%;
-    max-width: 1200px;
-    max-height: 700px;
-    display: flex;
-    flex-direction: column;
-    overflow: hidden;
-
-    .table-preview-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: 12px 16px;
-      border-bottom: 1px solid rgba(63, 80, 106, 0.3);
-      flex-shrink: 0;
-
-      .table-preview-title {
-        font-size: 15px;
-        font-weight: 500;
-        color: #e0e6ed;
-      }
-
-      .table-preview-close {
-        width: 32px;
-        height: 32px;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        border-radius: 4px;
-        color: #8b949e;
-        cursor: pointer;
-        transition: all 0.2s;
-
-        &:hover {
-          color: #e0e6ed;
-          background: rgba(10, 132, 255, 0.15);
-        }
-      }
-    }
-
-    :deep(.table-preview-body) {
-      padding: 20px;
-      overflow: auto;
-      flex: 1;
-
-      table {
-        width: 100%;
-        border-collapse: collapse;
-      }
-
-      th,
-      td {
-        border: 1px solid #3f506a;
-        padding: 10px 14px;
-        text-align: left;
-        color: #e0e6ed;
-        font-size: 14px;
-      }
-
-      th {
-        background: #1e2538;
-        font-weight: 600;
-      }
-
-      tbody tr {
-        &:nth-child(even) td {
-          background: rgba(10, 132, 255, 0.04);
-        }
-
-        &:hover td {
-          background: rgba(10, 132, 255, 0.1);
-        }
-      }
-    }
-  }
-
   .scroll-to-bottom {
     position: absolute;
     bottom: 20px;

+ 1 - 1
src/views/ventAI/manageAssistent/components/chatModal/CreateScheduleModal.vue

@@ -447,7 +447,7 @@
     }
 
     .zxm-modal-body {
-      padding: 24px;
+      padding: 24px !important;
     }
 
     .zxm-modal-close {

+ 1 - 1
src/views/ventAI/manageAssistent/components/chatModal/CreateSubAgentModal.vue

@@ -628,7 +628,7 @@
     }
 
     .zxm-modal-body {
-      padding: 24px;
+      padding: 24px !important;
       max-height: 70vh;
       overflow-y: auto;
     }

+ 118 - 2
src/views/ventAI/manageAssistent/components/chatModal/FilePreviewPanel.vue

@@ -85,7 +85,7 @@
           <div v-else class="no-preview-text">CSV文件内容为空</div>
         </div>
         <div v-else-if="isMdFile(file.name)" class="markdown-preview">
-          <div class="markdown-body" v-html="renderedMarkdown"></div>
+          <div class="markdown-body" v-html="renderedMarkdown" @click="handleTableAction"></div>
         </div>
         <div v-else-if="isTextFile(file.name)" class="text-preview">
           <pre>{{ file.content || '文件内容加载中...' }}</pre>
@@ -118,6 +118,7 @@
     getFileExtension,
     getFileIconVar,
     renderMarkdown,
+    wrapMarkdownCodeBlocks,
   } from './utils';
   import type { AttachedFile } from './types';
 
@@ -126,6 +127,13 @@
     return val.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
   };
 
+  const tableIcons = {
+    copy: getCssUrl('--img-chat-copy-icon'),
+    download: getCssUrl('--img-chat-download-icon-btn'),
+    preview: getCssUrl('--img-chat-preview-icon'),
+    copySuccess: getCssUrl('--img-chat-copy-success-icon'),
+  };
+
   const props = withDefaults(
     defineProps<{
       file: AttachedFile;
@@ -139,6 +147,7 @@
   const emit = defineEmits<{
     (e: 'close'): void;
     (e: 'save-success', content: string): void;
+    (e: 'open-markdown-tab', payload: { title: string; content: string }): void;
   }>();
 
   // 编辑模式
@@ -232,9 +241,77 @@
   });
 
   const renderedMarkdown = computed(() => {
-    return renderMarkdown(props.file.content || '');
+    return wrapMarkdownCodeBlocks(renderMarkdown(props.file.content || ''), tableIcons);
   });
 
+  // Markdown 代码块工具栏操作(复制/下载/预览)
+
+  const copyToClipboard = async (text: string) => {
+    try {
+      await navigator.clipboard.writeText(text);
+    } catch {
+      const ta = document.createElement('textarea');
+      ta.value = text;
+      ta.style.position = 'fixed';
+      ta.style.opacity = '0';
+      document.body.appendChild(ta);
+      ta.select();
+      document.execCommand('copy');
+      document.body.removeChild(ta);
+    }
+  };
+
+  const showCopySuccess = (btn: HTMLElement) => {
+    const img = btn.querySelector('img');
+    const successIcon = btn.dataset.iconSuccess;
+    const originalIcon = btn.dataset.icon;
+    if (img && successIcon) {
+      img.src = successIcon;
+      btn.title = '复制成功';
+      const wrapper = btn.closest('.markdown-code-wrapper')!;
+      const reset = () => {
+        img.src = originalIcon!;
+        btn.title = '复制内容';
+        wrapper.removeEventListener('mouseleave', reset);
+      };
+      wrapper.addEventListener('mouseleave', reset);
+    }
+  };
+
+  const handleTableAction = (e: Event) => {
+    const target = (e.target as HTMLElement).closest('.table-action-btn') as HTMLElement;
+    if (!target) return;
+
+    const action = target.dataset.action;
+    const encodedContent = target.dataset.content;
+    if (!action || !encodedContent) return;
+
+    const content = decodeURIComponent(encodedContent);
+
+    switch (action) {
+      case 'copy-md': {
+        copyToClipboard(content).then(() => showCopySuccess(target));
+        break;
+      }
+      case 'download-md': {
+        const blob = new Blob([content], { type: 'text/markdown;charset=utf-8;' });
+        const url = URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = `markdown_${Date.now()}.md`;
+        a.click();
+        URL.revokeObjectURL(url);
+        break;
+      }
+      case 'preview-md': {
+        // 在右侧面板新建标签页展示渲染效果
+        const title = content.match(/^#{1,3}\s+(.+?)\s*$/m)?.[1] || 'Markdown预览';
+        emit('open-markdown-tab', { title, content });
+        break;
+      }
+    }
+  };
+
   const csvRows = computed(() => {
     const content = props.file.content || '';
     if (!content.trim()) return [];
@@ -770,6 +847,45 @@
           border: none;
           border-top: 1px solid rgb(255 255 255 / 10%);
         }
+
+        :deep(.markdown-code-wrapper) {
+          position: relative;
+          margin: 6px 0;
+
+          .table-actions {
+            position: absolute;
+            top: 4px;
+            right: 4px;
+            display: flex;
+            gap: 2px;
+            opacity: 0;
+            transition: opacity 0.2s;
+            z-index: 1;
+          }
+
+          &:hover .table-actions {
+            opacity: 1;
+          }
+
+          .table-action-btn {
+            width: 26px;
+            height: 26px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            border-radius: 4px;
+            color: #8b949e;
+            cursor: pointer;
+            background: rgba(22, 27, 34, 0.8);
+            backdrop-filter: blur(4px);
+            transition: all 0.2s;
+
+            &:hover {
+              color: #e0e6ed;
+              background: rgba(10, 132, 255, 0.3);
+            }
+          }
+        }
       }
     }
 

+ 2 - 0
src/views/ventAI/manageAssistent/components/chatModal/RightPanelTabs.vue

@@ -53,6 +53,7 @@
         hide-header
         @close="closeActive"
         @save-success="handleSaveSuccess"
+        @open-markdown-tab="emit('open-markdown-tab', $event)"
       />
       <AgentDetailPanel
         v-else-if="activeTab?.type === 'agent'"
@@ -98,6 +99,7 @@
     (e: 'schedule-deleted'): void;
     (e: 'schedule-updated'): void;
     (e: 'open-schedule-session', sessionId: string, prompt: string): void;
+    (e: 'open-markdown-tab', payload: { title: string; content: string }): void;
   }>();
 
   const activeTab = computed(() => props.tabs.find((t) => t.id === props.activeTabId) || null);

+ 27 - 0
src/views/ventAI/manageAssistent/components/chatModal/utils.ts

@@ -91,6 +91,33 @@ export const wrapTables = (html: string, icons: { copy: string; download: string
   });
 };
 
+const decodeHtmlEntities = (escaped: string): string => {
+  const textarea = document.createElement('textarea');
+  textarea.innerHTML = escaped;
+  return textarea.value;
+};
+
+// 给渲染后 HTML 中的 ```markdown / ```md 代码块加上操作工具栏(复制/下载/预览)
+export const wrapMarkdownCodeBlocks = (html: string, icons: { copy: string; download: string; preview: string; copySuccess: string }): string => {
+  return html.replace(/<pre><code class="[^"]*language-(?:markdown|md)[^"]*">([\s\S]*?)<\/code><\/pre>/g, (match, escaped: string) => {
+    const encoded = encodeURIComponent(decodeHtmlEntities(escaped));
+    return `<div class="markdown-code-wrapper">
+      <div class="table-actions">
+        <span class="table-action-btn" data-action="copy-md" data-content="${encoded}" data-icon="${icons.copy}" data-icon-success="${icons.copySuccess}" title="复制内容">
+          <img src="${icons.copy}" width="14" height="14" />
+        </span>
+        <span class="table-action-btn" data-action="download-md" data-content="${encoded}" title="下载.md文件">
+          <img src="${icons.download}" width="14" height="14" />
+        </span>
+        <span class="table-action-btn" data-action="preview-md" data-content="${encoded}" title="预览渲染效果">
+          <img src="${icons.preview}" width="14" height="14" />
+        </span>
+      </div>
+      ${match}
+    </div>`;
+  });
+};
+
 export const tableHtmlToMarkdown = (tableHtml: string): string => {
   const parser = new DOMParser();
   const doc = parser.parseFromString(tableHtml, 'text/html');