浏览代码

[Feat 0000]AI助手右侧面板实现多标签页

wangkeyi 4 天之前
父节点
当前提交
2266887b70

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/5-2.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1788932475432" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10431" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 828.16l350.08-350.08c18.56-18.56 49.28-18.56 67.84 0s18.56 49.28 0 67.84l-384 384c-18.56 18.56-49.28 18.56-67.84 0l-384-384c-9.6-9.6-14.08-21.76-14.08-33.92s4.48-24.32 14.08-33.92c18.56-18.56 49.28-18.56 67.84 0L512 828.16z" p-id="10432" fill="#ffffff"></path><path d="M512 444.16l350.08-350.08c18.56-18.56 49.28-18.56 67.84 0s18.56 49.28 0 67.84l-384 384c-18.56 18.56-49.28 18.56-67.84 0l-384-384c-9.6-9.6-14.08-21.76-14.08-33.92s4.48-24.32 14.08-33.92c18.56-18.56 49.28-18.56 67.84 0L512 444.16z" p-id="10433" fill="#ffffff"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/5-3.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1788933965154" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8225" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M613.096727 0a93.090909 93.090909 0 0 1 65.815273 27.275636l224.721455 224.721455A93.090909 93.090909 0 0 1 930.909091 317.812364V837.818182a186.181818 186.181818 0 0 1-186.181818 186.181818H279.272727a186.181818 186.181818 0 0 1-186.181818-186.181818V186.181818a186.181818 186.181818 0 0 1 186.181818-186.181818h333.824z m-52.363636 425.751273l-52.130909 147.828363-53.294546-147.409454a27.927273 27.927273 0 0 0-15.778909-16.384l-4.514909-1.396364a27.927273 27.927273 0 0 0-33.233454 21.317818l-52.130909 238.871273a23.272727 23.272727 0 1 0 45.428363 9.914182l38.4-175.802182 49.105455 135.68a27.927273 27.927273 0 0 0 31.371636 17.966546l4.375273-1.163637a27.927273 27.927273 0 0 0 16.849454-16.989091l48.826182-138.565818 45.056 179.572364a23.272727 23.272727 0 0 0 24.064 17.594182l4.189091-0.651637a23.272727 23.272727 0 0 0 16.896-28.253091l-60.043636-239.616a27.927273 27.927273 0 0 0-53.434182-2.513454z" fill="#ffffff" p-id="8226" data-spm-anchor-id="a313x.search_index.0.i24.3d143a81xfzOBJ" class=""></path><path d="M651.636364 0l279.272727 279.272727h-186.181818a93.090909 93.090909 0 0 1-93.090909-93.090909V0z" fill="#e6e6e6" opacity=".6" p-id="8227" data-spm-anchor-id="a313x.search_index.0.i25.3d143a81xfzOBJ" class="selected"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/5-4.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1788932910037" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2672" data-spm-anchor-id="a313x.collections_detail.0.i7.327a3a81ZB3A2v" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M204.8 0h477.866667l273.066666 273.066667v614.4c0 75.093333-61.44 136.533333-136.533333 136.533333H204.8c-75.093333 0-136.533333-61.44-136.533333-136.533333V136.533333C68.266667 61.44 129.706667 0 204.8 0z m354.986667 641.706667L682.666667 518.826667c13.653333-13.653333 13.653333-34.133333 0-47.786667-20.48-6.826667-40.96-6.826667-54.613334 6.826667L505.173333 593.92 389.12 477.866667c-13.653333-13.653333-34.133333-13.653333-47.786667 0-13.653333 13.653333-13.653333 34.133333 0 47.786666l122.88 122.88L341.333333 764.586667c-13.653333 13.653333-13.653333 34.133333 0 47.786666 13.653333 13.653333 34.133333 13.653333 47.786667 0l122.88-122.88 122.88 122.88c13.653333 13.653333 34.133333 13.653333 47.786667 0 13.653333-13.653333 13.653333-34.133333 0-47.786666L559.786667 641.706667z" fill="#ffffff" p-id="2673" data-spm-anchor-id="a313x.collections_detail.0.i4.327a3a81ZB3A2v" class="selected"></path><path d="M682.666667 0l273.066666 273.066667h-204.8c-40.96 0-68.266667-27.306667-68.266666-68.266667V0z" fill="#e6e6e6" opacity=".619" p-id="2674" data-spm-anchor-id="a313x.collections_detail.0.i6.327a3a81ZB3A2v" class="selected"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/5-5.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1788934008636" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3100" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M204.8 0h477.866667l273.066666 273.066667v614.4c0 75.093333-61.44 136.533333-136.533333 136.533333H204.8c-75.093333 0-136.533333-61.44-136.533333-136.533333V136.533333C68.266667 61.44 129.706667 0 204.8 0z m307.2 607.573333l68.266667 191.146667c13.653333 27.306667 54.613333 27.306667 61.44 0l102.4-273.066667c6.826667-20.48 0-34.133333-20.48-40.96s-34.133333 0-40.96 13.653334l-68.266667 191.146666-68.266667-191.146666c-13.653333-27.306667-54.613333-27.306667-68.266666 0l-68.266667 191.146666-68.266667-191.146666c-6.826667-13.653333-27.306667-27.306667-47.786666-20.48s-27.306667 27.306667-20.48 47.786666l102.4 273.066667c13.653333 27.306667 54.613333 27.306667 61.44 0l75.093333-191.146667z" fill="#ffffff" p-id="3101" data-spm-anchor-id="a313x.collections_detail.0.i10.327a3a81ZB3A2v" class="selected"></path><path d="M682.666667 0l273.066666 273.066667h-204.8c-40.96 0-68.266667-27.306667-68.266666-68.266667V0z" fill="#e6e6e6" opacity=".619" p-id="3102" data-spm-anchor-id="a313x.collections_detail.0.i11.327a3a81ZB3A2v" class="selected"></path></svg>

文件差异内容过多而无法显示
+ 0 - 0
src/assets/images/ventAI/chatModalBlue/5-6.svg


+ 1 - 0
src/assets/images/ventAI/chatModalBlue/5-7.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1788934147750" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3624" data-spm-anchor-id="a313x.collections_detail.0.i17.327a3a81ZB3A2v" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M204.8 0h477.866667l273.066666 273.066667v614.4c0 75.093333-61.44 136.533333-136.533333 136.533333H204.8c-75.093333 0-136.533333-61.44-136.533333-136.533333V136.533333C68.266667 61.44 129.706667 0 204.8 0z m341.333333 512h136.533334c20.48 0 34.133333-13.653333 34.133333-34.133333s-13.653333-34.133333-34.133333-34.133334H341.333333c-20.48 0-34.133333 13.653333-34.133333 34.133334s13.653333 34.133333 34.133333 34.133333h136.533334V819.2c0 20.48 13.653333 34.133333 34.133333 34.133333s34.133333-13.653333 34.133333-34.133333V512z" fill="#ffffff" p-id="3625" data-spm-anchor-id="a313x.collections_detail.0.i16.327a3a81ZB3A2v" class="selected"></path><path d="M682.666667 0l273.066666 273.066667h-204.8c-40.96 0-68.266667-27.306667-68.266666-68.266667V0z" fill="#e6e6e6" opacity=".619" p-id="3626" data-spm-anchor-id="a313x.collections_detail.0.i18.327a3a81ZB3A2v" class="selected"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/5-8.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1788934355906" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4051" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M136.533333 0h750.933334c75.093333 0 136.533333 61.44 136.533333 136.533333v750.933334c0 75.093333-61.44 136.533333-136.533333 136.533333H136.533333c-75.093333 0-136.533333-61.44-136.533333-136.533333V136.533333C0 61.44 61.44 0 136.533333 0z m580.266667 409.6c54.613333 0 102.4-47.786667 102.4-102.4S771.413333 204.8 716.8 204.8 614.4 252.586667 614.4 307.2 662.186667 409.6 716.8 409.6z m-273.066667 266.24l-68.266666-109.226667c0-6.826667-6.826667-6.826667-13.653334-13.653333-20.48-6.826667-40.96-6.826667-47.786666 13.653333l-177.493334 273.066667v13.653333c0 20.48 13.653333 34.133333 34.133334 34.133334h696.32c13.653333 0 27.306667-13.653333 27.306666-27.306667 0-6.826667 0-13.653333-6.826666-13.653333l-150.186667-211.626667c-6.826667-13.653333-27.306667-13.653333-40.96-6.826667l-6.826667 6.826667-13.653333 13.653333-95.573333 150.186667c-13.653333 13.653333-34.133333 20.48-47.786667 6.826667-6.826667 0-6.826667-6.826667-6.826667-13.653334L443.733333 675.84z" fill="#ffffff" p-id="4052" data-spm-anchor-id="a313x.collections_detail.0.i21.327a3a81ZB3A2v" class="selected"></path></svg>

+ 16 - 2
src/design/themify/deepblue.less

@@ -67,7 +67,7 @@ html[data-theme='deepblue'] {
   // AI助手悬浮按钮图片
   --img-ai-float: url('/src/assets/images/ventAI/chatModalBlue/4-1.png');
 
-  // 聊天弹窗图片
+ // 聊天弹窗图片
   --img-chat-modal-bg: url('/src/assets/images/ventAI/chatModalBlue/1-1.png');
   --img-chat-close-bg: url('/src/assets/images/ventAI/chatModalBlue/1-2.png');
   --img-chat-options-icon-border: url('/src/assets/images/ventAI/chatModalBlue/1-3.png');
@@ -76,7 +76,11 @@ html[data-theme='deepblue'] {
   --img-chat-options-add-icon: url('/src/assets/images/ventAI/chatModalBlue/1-7.svg');
   --img-chat-options-search-icon: url('/src/assets/images/ventAI/chatModalBlue/1-8.svg');
   --img-chat-options-skill-icon: url('/src/assets/images/ventAI/chatModalBlue/1-9.svg');
+  --img-chat-options-schedule-icon: url('/src/assets/images/ventAI/chatModalBlue/4-8.svg');
   --img-chat-options-sub-agent-icon: url('/src/assets/images/ventAI/chatModalBlue/4-12.svg');
+  --img-chat-schedule-empty-icon: url('/src/assets/images/ventAI/chatModalBlue/4-9.svg');
+  --img-chat-schedule-run-icon: url('/src/assets/images/ventAI/chatModalBlue/4-10.svg');
+  --img-chat-schedule-more-icon: url('/src/assets/images/ventAI/chatModalBlue/4-11.svg');
   --img-chat-expand-icon: url('/src/assets/images/ventAI/chatModalBlue/1-10.svg');
   --img-chat-collapse-icon: url('/src/assets/images/ventAI/chatModalBlue/1-11.svg');
   --img-chat-prev-icon: url('/src/assets/images/ventAI/chatModalBlue/1-14.png');
@@ -88,8 +92,9 @@ html[data-theme='deepblue'] {
   --img-chat-agent-icon: url('/src/assets/images/ventAI/chatModalBlue/2-4.svg');
   --img-chat-copy-success-icon: url('/src/assets/images/ventAI/chatModalBlue/2-5.svg');
   --img-chat-send-icon: url('/src/assets/images/ventAI/chatModalBlue/2-6.svg');
+  --img-chat-stop-icon: url('/src/assets/images/ventAI/chatModalBlue/2-7.svg');
   --img-chat-download-icon: url('/src/assets/images/ventAI/chatModalBlue/2-8.png');
-  --img-chat-input-bg: url('/src/assets/images/ventAI/chatModalBlue/2-13.png');
+  --img-chat-input-bg: url('/src/assets/images/ventAI/chatModalBlue/1-7.svg');
   --img-chat-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-7.svg');
   --img-chat-popup-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-3.svg');
   --img-chat-popup-mention-icon: url('/src/assets/images/ventAI/chatModalBlue/3-4.svg');
@@ -101,7 +106,16 @@ html[data-theme='deepblue'] {
   --img-chat-close-icon-btn: url('/src/assets/images/ventAI/chatModalBlue/4-4.svg');
   --img-chat-download-icon-btn: url('/src/assets/images/ventAI/chatModalBlue/4-5.svg');
   --img-chat-preview-icon: url('/src/assets/images/ventAI/chatModalBlue/4-6.svg');
+  --img-chat-session-approve-icon: url('/src/assets/images/ventAI/chatModalBlue/4-7.svg');
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
+  --img-chat-tabs-icon: url('/src/assets/images/ventAI/chatModalBlue/5-2.svg');
+  // 文件类型图标
+  --img-chat-file-md-icon: url('/src/assets/images/ventAI/chatModalBlue/5-3.svg');
+  --img-chat-file-excel-icon: url('/src/assets/images/ventAI/chatModalBlue/5-4.svg');
+  --img-chat-file-word-icon: url('/src/assets/images/ventAI/chatModalBlue/5-5.svg');
+  --img-chat-file-pdf-icon: url('/src/assets/images/ventAI/chatModalBlue/5-6.svg');
+  --img-chat-file-txt-icon: url('/src/assets/images/ventAI/chatModalBlue/5-7.svg');
+  --img-chat-file-img-icon: url('/src/assets/images/ventAI/chatModalBlue/5-8.svg');
   --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
   --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');
   --img-chat-edit-icon: url('/src/assets/images/ventAI/chatModalBlue/2-9.svg');

+ 8 - 0
src/design/themify/default.less

@@ -115,6 +115,14 @@ html {
   --img-chat-preview-icon: url('/src/assets/images/ventAI/chatModalBlue/4-6.svg');
   --img-chat-session-approve-icon: url('/src/assets/images/ventAI/chatModalBlue/4-7.svg');
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
+  --img-chat-tabs-icon: url('/src/assets/images/ventAI/chatModalBlue/5-2.svg');
+  // 文件类型图标
+  --img-chat-file-md-icon: url('/src/assets/images/ventAI/chatModalBlue/5-3.svg');
+  --img-chat-file-excel-icon: url('/src/assets/images/ventAI/chatModalBlue/5-4.svg');
+  --img-chat-file-word-icon: url('/src/assets/images/ventAI/chatModalBlue/5-5.svg');
+  --img-chat-file-pdf-icon: url('/src/assets/images/ventAI/chatModalBlue/5-6.svg');
+  --img-chat-file-txt-icon: url('/src/assets/images/ventAI/chatModalBlue/5-7.svg');
+  --img-chat-file-img-icon: url('/src/assets/images/ventAI/chatModalBlue/5-8.svg');
   --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
   --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');
   --img-chat-edit-icon: url('/src/assets/images/ventAI/chatModalBlue/2-9.svg');

+ 140 - 111
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -104,43 +104,20 @@
           />
         </div>
 
-        <!-- 文件预览面板(与 chat-panel 并列,带滑入滑出动画) -->
+        <!-- 右侧辅助面板(标签页容器,与 chat-panel 并列,带滑入滑出动画) -->
         <Transition name="right-panel-slide" @after-leave="onPanelLeaveEnd">
-          <div v-if="previewFile" class="right-panel">
-            <FilePreviewPanel
-              :file="previewFile"
-              :editable="previewEditable"
-              :save-filename="previewFile?.name || ''"
-              @close="removePreviewFile"
-              @save-success="handleSaveSuccess"
-            />
-          </div>
-        </Transition>
-
-        <!-- Agent 详情面板(与 chat-panel 并列,带滑入滑出动画) -->
-        <Transition name="right-panel-slide" @after-leave="onPanelLeaveEnd">
-          <div v-if="activeAgentDetail" class="right-panel">
-            <AgentDetailPanel
-              :messageIndex="activeAgentDetail.messageIndex"
-              :agentName="activeAgentDetail.agent"
-              :agentId="activeAgentDetail.agentId"
-              :messages="messages"
-              @close="closeAgentDetail"
-            />
-          </div>
-        </Transition>
-
-        <!-- 定时任务详情面板(与 chat-panel 并列,带滑入滑出动画) -->
-        <Transition name="right-panel-slide" @after-leave="onPanelLeaveEnd">
-          <div v-if="currentScheduleTask" class="right-panel narrow">
-            <ScheduleDetailPanel
-              :task="currentScheduleTask"
-              @close="showScheduleDetail = false"
-              @deleted="handleScheduleDeleted"
-              @updated="handleScheduleUpdated"
-              @open-session="handleOpenScheduleSession"
-            />
-          </div>
+          <RightPanelTabs
+            v-if="currentRightPanels.tabs.length > 0"
+            :tabs="currentRightPanels.tabs"
+            :activeTabId="currentRightPanels.activeTabId"
+            :messages="messages"
+            @switch-tab="activateTab"
+            @close-tab="closeTab"
+            @save-success="handleSaveSuccess"
+            @schedule-deleted="handleScheduleDeleted"
+            @schedule-updated="handleScheduleUpdated"
+            @open-schedule-session="handleOpenScheduleSession"
+          />
         </Transition>
       </template>
 
@@ -193,17 +170,15 @@
   import FileListPanel from './chatModal/FileListPanel.vue';
   import ChatMessages from './chatModal/ChatMessages.vue';
   import type { AgentDetailData } from './chatModal/ChatMessages.vue';
-  import AgentDetailPanel from './chatModal/AgentDetailPanel.vue';
   import ChatInputArea from './chatModal/ChatInputArea.vue';
-  import FilePreviewPanel from './chatModal/FilePreviewPanel.vue';
   import TodoListBar from './chatModal/TodoListBar.vue';
   import SkillPanel from './chatModal/SkillPanel.vue';
   import SubAgentPanel from './chatModal/SubAgentPanel.vue';
   import SchedulePanel from './chatModal/SchedulePanel.vue';
-  import ScheduleDetailPanel from './chatModal/ScheduleDetailPanel.vue';
+  import RightPanelTabs from './chatModal/RightPanelTabs.vue';
   import { getScheduleList } from '../api';
   import type { TodayUsage } from '../api';
-  import type { AttachedFile, Message, Task, SseEvent } from './chatModal/types';
+  import type { AttachedFile, Message, Task, RightPanelTab, RightPanelState, SseEvent } from './chatModal/types';
   import { isTextFile, isVideoFile, isAudioFile, isWordFile, isExcelFile, isCsvFile } from './chatModal/utils';
 
   interface Props {
@@ -294,20 +269,9 @@
   const chatInputRef = ref<InstanceType<typeof ChatInputArea>>();
   const chatMessagesRef = ref<InstanceType<typeof ChatMessages>>();
 
-  const previewFile = ref<AttachedFile | null>(null);
-  const previewEditable = ref(false);
   const showFileList = ref(false);
-  const activeAgentDetail = ref<{ messageIndex: number; agent: string; agentId?: string } | null>(null);
   const activePanel = ref<'chat' | 'skill' | 'sub-agents' | 'schedules'>('chat');
   const scheduleListForDetail = ref<any[]>([]);
-  const showScheduleDetail = ref(false);
-
-  const currentScheduleTask = computed(() => {
-    if (!showScheduleDetail.value || activePanel.value !== 'chat') return null;
-    const sessionId = currentTask.value?.sessionId;
-    if (!sessionId) return null;
-    return scheduleListForDetail.value.find((t: any) => t.feedback_session_id === sessionId) || null;
-  });
 
   const fetchScheduleListForDetail = async () => {
     try {
@@ -373,7 +337,6 @@
       activePanel.value = 'chat';
     } else {
       activePanel.value = panel;
-      previewFile.value = null;
     }
   };
 
@@ -384,8 +347,6 @@
 
   const handleOpenScheduleSession = async (sessionId: string, prompt?: string) => {
     activePanel.value = 'chat';
-    previewFile.value = null;
-    showScheduleDetail.value = true;
     await fetchSessionList();
     await fetchScheduleListForDetail();
     const existing = taskList.value.find((t) => t.sessionId === sessionId);
@@ -415,13 +376,24 @@
   const schedulePanelRef = ref();
 
   const handleScheduleDeleted = () => {
-    showScheduleDetail.value = false;
+    const task = currentTask.value;
+    if (task?.rightPanels) {
+      const tab = task.rightPanels.tabs.find((t) => t.type === 'schedule');
+      if (tab) removeTab(task.rightPanels, tab.id);
+    }
     fetchScheduleListForDetail();
     schedulePanelRef.value?.fetchScheduleList?.();
   };
 
-  const handleScheduleUpdated = () => {
-    fetchScheduleListForDetail();
+  const handleScheduleUpdated = async () => {
+    await fetchScheduleListForDetail();
+    // 同步标签内任务对象为最新数据(编辑/启停后)
+    const task = currentTask.value;
+    const tab = task?.rightPanels?.tabs.find((t) => t.type === 'schedule');
+    if (tab?.type === 'schedule') {
+      const fresh = scheduleListForDetail.value.find((s: any) => s.id === tab.task.id);
+      if (fresh) tab.task = fresh;
+    }
     schedulePanelRef.value?.fetchScheduleList?.();
   };
 
@@ -446,10 +418,17 @@
   };
   window.addEventListener('resize', onResize);
 
-  // 是否有右侧辅助面板(文件预览/Agent详情/定时任务详情)与 chat-panel 并列显示
+  const currentTask = computed(() => taskList.value.find((t) => t.id === currentTaskId.value));
+
+  const currentRightPanels = computed<RightPanelState>(() => {
+    const task = currentTask.value;
+    return task?.rightPanels || { tabs: [], activeTabId: null };
+  });
+
+  // 是否有右侧辅助面板(标签页容器)与 chat-panel 并列显示
   const hasRightPanel = computed(() => {
     if (activePanel.value !== 'chat') return false;
-    return !!(previewFile.value || activeAgentDetail.value || currentScheduleTask.value);
+    return currentRightPanels.value.tabs.length > 0;
   });
 
   // 面板离开动画标记:确保 modal 在面板退出动画结束后才收缩宽度
@@ -585,9 +564,6 @@
 
     inputMessage.value = '';
     pendingFile.value = null;
-    previewFile.value = null;
-    activeAgentDetail.value = null;
-    showScheduleDetail.value = false;
     showFileList.value = false;
 
     if (newTask.sessionId && !historyLoadedTasks.has(taskId)) {
@@ -617,11 +593,19 @@
     messages.value = newTask.messages && newTask.messages.length > 0 ? [...newTask.messages] : getDefaultMessages();
     syncTodoBarFromMessages();
 
-    // 切换任务时检测是否为定时任务会话
-    if (newTask.sessionId && scheduleListForDetail.value.some((s: any) => s.feedback_session_id === newTask.sessionId)) {
-      showScheduleDetail.value = true;
-    } else {
-      showScheduleDetail.value = false;
+    // 切换任务时检测是否为定时任务会话:自动打开/关闭定时任务详情标签
+    const scheduleItem = newTask.sessionId ? scheduleListForDetail.value.find((s: any) => s.feedback_session_id === newTask.sessionId) : undefined;
+    const panelState = getPanelState(newTask);
+    const existingScheduleTab = panelState.tabs.find((t) => t.type === 'schedule');
+    if (scheduleItem) {
+      if (existingScheduleTab) {
+        panelState.activeTabId = existingScheduleTab.id;
+      } else {
+        panelState.tabs.push({ id: 'schedule', type: 'schedule', title: scheduleItem.name || '定时任务', task: scheduleItem });
+        panelState.activeTabId = 'schedule';
+      }
+    } else if (existingScheduleTab) {
+      removeTab(panelState, existingScheduleTab.id);
     }
 
     await nextTick();
@@ -1046,40 +1030,48 @@
     pendingFile.value = null;
   };
 
-  const removePreviewFile = () => {
-    previewFile.value = null;
-    previewEditable.value = false;
-    // 关闭文件预览后,恢复定时任务详情面板
-    const sessionId = currentTask.value?.sessionId;
-    if (sessionId && scheduleListForDetail.value.some((s: any) => s.feedback_session_id === sessionId)) {
-      showScheduleDetail.value = true;
+  const openFileTab = (file: AttachedFile, editable: boolean) => {
+    const task = currentTask.value;
+    if (!task) return;
+    const state = getPanelState(task);
+    const existing = state.tabs.find((t) => t.type === 'file' && t.file.id === file.id);
+    if (existing) {
+      // 编辑入口可升级已有标签为可编辑;预览入口不降级
+      if (editable) existing.editable = true;
+      state.activeTabId = existing.id;
+      return;
     }
+    openTab({ id: nextTabId(), type: 'file', title: file.name, file, editable });
   };
 
   const handleAgentDetailClick = (data: AgentDetailData) => {
-    // 同一个 agent 再次点击则关闭面板
-    if (
-      activeAgentDetail.value?.messageIndex === data.messageIndex &&
-      activeAgentDetail.value?.agentId === data.agentId &&
-      activeAgentDetail.value?.agent === data.agent
-    ) {
-      activeAgentDetail.value = null;
-    } else {
-      activeAgentDetail.value = { messageIndex: data.messageIndex, agent: data.agent, agentId: data.agentId };
-      previewFile.value = null;
-      showScheduleDetail.value = false;
+    const task = currentTask.value;
+    if (!task) return;
+    const state = getPanelState(task);
+    const existing = state.tabs.find(
+      (t) => t.type === 'agent' && t.messageIndex === data.messageIndex && (t.agentId || '') === (data.agentId || '') && t.agent === data.agent
+    );
+    if (existing) {
+      if (state.activeTabId === existing.id) {
+        // 保持原交互:再次点击已激活的 agent 则关闭
+        removeTab(state, existing.id);
+      } else {
+        state.activeTabId = existing.id;
+      }
+      return;
     }
-  };
-
-  const closeAgentDetail = () => {
-    activeAgentDetail.value = null;
+    openTab({
+      id: nextTabId(),
+      type: 'agent',
+      title: data.agent,
+      messageIndex: data.messageIndex,
+      agent: data.agent,
+      agentId: data.agentId,
+    });
   };
 
   const openFilePreview = (file: AttachedFile) => {
-    previewFile.value = file;
-    previewEditable.value = false;
-    showScheduleDetail.value = false;
-    activeAgentDetail.value = null;
+    openFileTab(file, false);
 
     const msgIndex = messages.value.findIndex((m) => m.attachedFile?.id === file.id);
     if (msgIndex !== -1) {
@@ -1100,7 +1092,47 @@
     return currentTask?.attachedFiles || [];
   };
 
-  const currentTask = computed(() => taskList.value.find((t) => t.id === currentTaskId.value));
+  // 右侧辅助面板标签页状态(按任务维度保存,切换任务自动恢复)
+  let tabSeq = 0;
+  const nextTabId = () => `panel-${Date.now()}-${tabSeq++}`;
+
+  const getPanelState = (task: Task): RightPanelState => {
+    if (!task.rightPanels) {
+      task.rightPanels = { tabs: [], activeTabId: null };
+    }
+    return task.rightPanels;
+  };
+
+  const openTab = (tab: RightPanelTab) => {
+    const task = currentTask.value;
+    if (!task) return;
+    const state = getPanelState(task);
+    state.tabs.push(tab);
+    state.activeTabId = tab.id;
+  };
+
+  const activateTab = (tabId: string) => {
+    const task = currentTask.value;
+    if (!task?.rightPanels) return;
+    if (task.rightPanels.tabs.some((t) => t.id === tabId)) {
+      task.rightPanels.activeTabId = tabId;
+    }
+  };
+
+  const removeTab = (state: RightPanelState, tabId: string) => {
+    const idx = state.tabs.findIndex((t) => t.id === tabId);
+    if (idx === -1) return;
+    state.tabs.splice(idx, 1);
+    if (state.activeTabId === tabId) {
+      state.activeTabId = state.tabs.length ? (state.tabs[idx] || state.tabs[idx - 1]).id : null;
+    }
+  };
+
+  const closeTab = (tabId: string) => {
+    const task = currentTask.value;
+    if (!task?.rightPanels) return;
+    removeTab(task.rightPanels, tabId);
+  };
 
   const currentWordUrl = computed(() => {
     return currentTask.value?.wordUrl || '';
@@ -1136,15 +1168,14 @@
   const downloadWordReport = () => downloadWordFile(currentWordUrl.value);
 
   const handleOpenFilePreview = (file: AttachedFile) => {
-    previewFile.value = file;
-    previewEditable.value = true;
-    showScheduleDetail.value = false;
-    activeAgentDetail.value = null;
+    openFileTab(file, true);
   };
 
   const handleSaveSuccess = (content: string) => {
-    if (previewFile.value) {
-      previewFile.value = { ...previewFile.value, content };
+    const task = currentTask.value;
+    const tab = task?.rightPanels?.tabs.find((t) => t.id === task.rightPanels?.activeTabId);
+    if (tab?.type === 'file') {
+      tab.file = { ...tab.file, content };
     }
   };
 
@@ -1389,8 +1420,11 @@
             timestamp: Date.now(),
           });
         }
-        currentTodos.value = [...(data.todos || [])];
-        showTodoBar.value = true;
+        // todos 与任务绑定:仅当用户正在查看该任务时才更新进度条,避免流式期间切换/新建任务后串台
+        if (isViewingTask) {
+          currentTodos.value = [...(data.todos || [])];
+          showTodoBar.value = true;
+        }
         break;
       }
 
@@ -1885,8 +1919,8 @@
         }
 
         messages.value = targetTask.messages && targetTask.messages.length > 0 ? [...targetTask.messages] : getDefaultMessages();
+        syncTodoBarFromMessages();
 
-        previewFile.value = null;
         pendingFile.value = null;
         showFileList.value = false;
         inputMessage.value = '';
@@ -2078,11 +2112,6 @@
       width: auto;
       min-width: 0;
     }
-
-    &.narrow {
-      width: 320px;
-      flex-shrink: 0;
-    }
     flex-shrink: 0;
   }
 
@@ -2339,14 +2368,14 @@
       .right-panel {
         background-image: var(--img-chat-fs-panel-bg) !important;
       }
+      :deep(.panel-tab-bar) {
+        padding: 14px 16px 0;
+      }
       .schedule-panel,
       .skill-panel,
       .sub-agent-panel {
         background-image: var(--img-chat-fs-panel-bg) !important;
       }
-      .schedule-detail-panel {
-        background-image: var(--img-chat-fs-task-panel-bg) !important;
-      }
     }
 
     .zxm-modal-header {

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

@@ -1,6 +1,6 @@
 <template>
   <div v-if="detailData" class="agent-detail-panel">
-    <div class="agent-detail-header">
+    <div v-if="!hideHeader" class="agent-detail-header">
       <div class="agent-detail-header-left">
         <div class="agent-detail-icon"></div>
         <span class="agent-detail-title">{{ detailData.agent }}</span>
@@ -102,6 +102,7 @@
     agentName: string;
     agentId?: string;
     messages: Message[];
+    hideHeader?: boolean;
   }>();
 
   const emit = defineEmits<{

+ 4 - 4
src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue

@@ -2,7 +2,7 @@
   <div class="chat-input-area">
     <div v-if="pendingFile" class="pending-file-display">
       <div class="pending-file-card" @click="emit('pending-file-preview', pendingFile)">
-        <div class="file-icon-pending"></div>
+        <div class="file-icon-pending" :style="{ backgroundImage: getFileIconVar(pendingFile.name) }"></div>
         <div class="file-info-pending">
           <div class="file-name-pending">{{ pendingFile.name }}</div>
           <div class="file-size-pending">{{ formatFileSize(pendingFile.size) }}</div>
@@ -193,11 +193,11 @@
                   <span class="usage-model-cost">{{ formatTokens(item.tokens) }}</span>
                 </div> -->
                 <div class="usage-model-row">
-                  <span class="usage-model-label">输入命中</span>
+                  <span class="usage-model-label">缓存命中</span>
                   <span class="usage-model-value">{{ formatTokens(item.cache_hit_tokens) }}</span>
                 </div>
                 <div class="usage-model-row">
-                  <span class="usage-model-label">输入未命中</span>
+                  <span class="usage-model-label">缓存未命中</span>
                   <span class="usage-model-value">{{ formatTokens(item.cache_miss_tokens) }}</span>
                 </div>
                 <div class="usage-model-row">
@@ -334,7 +334,7 @@
   import { ref, computed, watch, nextTick, onMounted } from 'vue';
   import { onClickOutside } from '@vueuse/core';
   import { CloseOutlined } from '@ant-design/icons-vue';
-  import { formatFileSize } from './utils';
+  import { formatFileSize, getFileIconVar } from './utils';
   import { getSkills } from '../../api';
   import type { UsageByModel } from '../../api';
   import type { AttachedFile, Task } from './types';

+ 68 - 13
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -113,20 +113,20 @@
 
             <!-- 文件操作卡片 -->
             <div v-if="message.downloadFiles?.length" class="download-file-cards">
-              <div v-for="(file, fIdx) in message.downloadFiles" :key="fIdx" class="download-file-card">
+              <div v-for="(file, fIdx) in message.downloadFiles" :key="fIdx" class="download-file-card" @click="handlePreviewFile(file)">
                 <div class="file-card-info">
-                  <div class="file-card-icon"></div>
+                  <div class="file-card-icon" :style="{ backgroundImage: getFileIconVar(file.filename) }"></div>
                   <div class="file-card-text">
                     <span class="file-card-name">{{ file.filename }}</span>
-                    <span class="file-card-hint">点击操作文件</span>
+                    <span class="file-card-hint">点击预览文件</span>
                   </div>
                 </div>
                 <div class="file-card-actions">
-                  <button class="file-action-btn download-btn" @click="handleDownloadFile(file.url, file.filename)">
+                  <button class="file-action-btn download-btn" @click.stop="handleDownloadFile(file.url, file.filename)">
                     <img :src="getCssUrl('--img-chat-download-icon-btn')" width="14" height="14" />
                     <span>下载</span>
                   </button>
-                  <button v-if="isEditableFile(file.filename)" class="file-action-btn edit-btn" @click="handleEditFile(file.url, file.filename)">
+                  <button v-if="isEditableFile(file.filename)" class="file-action-btn edit-btn" @click.stop="handleEditFile(file.url, file.filename)">
                     <img :src="getCssUrl('--img-chat-edit-icon')" width="14" height="14" />
                     <span>编辑</span>
                   </button>
@@ -279,7 +279,7 @@
           <div class="message-body">
             <div class="message-text" v-html="renderMarkdown(message.content, tableIcons)"></div>
             <div v-if="message.attachedFile" class="attached-file-card" @click="emit('file-preview', message.attachedFile)">
-              <div class="file-icon-card"></div>
+              <div class="file-icon-card" :style="{ backgroundImage: getFileIconVar(message.attachedFile.name) }"></div>
               <div class="file-info-card">
                 <div class="file-name-card">{{ message.attachedFile.name }}</div>
                 <div class="file-size-card">{{ formatFileSize(message.attachedFile.size) }}</div>
@@ -320,7 +320,19 @@
 <script setup lang="ts">
   import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
   import dayjs from 'dayjs';
-  import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv, isMdFile, isTextFile } from './utils';
+  import {
+    renderMarkdown,
+    formatFileSize,
+    tableHtmlToMarkdown,
+    tableHtmlToCsv,
+    isMdFile,
+    isTextFile,
+    isWordFile,
+    isExcelFile,
+    isImageFile,
+    isPdfFile,
+    getFileIconVar,
+  } from './utils';
   import { getToken } from '/@/utils/auth';
   import { message } from 'ant-design-vue';
   import type { Message, AttachedFile, ThinkingStep } from './types';
@@ -455,7 +467,7 @@
         csv: 'text/csv',
       };
       const file: AttachedFile = {
-        id: `report-${Date.now()}`,
+        id: `report-${url}`,
         name: filename,
         size: content.length,
         type: mimeMap[ext] || 'text/plain',
@@ -469,6 +481,53 @@
     }
   };
 
+  // 点击文件卡片:按类型拉取内容并在右侧辅助面板打开预览标签
+  const handlePreviewFile = async (file: { url: string; filename: string }) => {
+    if (!file.url) return;
+    try {
+      const resp = await fetch(file.url, { headers: { 'X-Access-Token': getToken() } });
+      if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+      const ext = file.filename.split('.').pop()?.toLowerCase() || '';
+      const mimeMap: Record<string, string> = {
+        md: 'text/markdown',
+        txt: 'text/plain',
+        json: 'application/json',
+        xml: 'text/xml',
+        yaml: 'text/yaml',
+        yml: 'text/yaml',
+        csv: 'text/csv',
+      };
+      const id = `report-${file.url}`;
+      const uploadTime = new Date().toISOString();
+
+      let previewFile: AttachedFile;
+      if (mimeMap[ext]) {
+        const content = await resp.text();
+        previewFile = { id, name: file.filename, size: content.length, type: mimeMap[ext], content, uploadTime };
+      } else if (isWordFile(file.filename) || isExcelFile(file.filename)) {
+        const arrayBuffer = await resp.arrayBuffer();
+        previewFile = { id, name: file.filename, size: arrayBuffer.byteLength, type: 'application/octet-stream', arrayBuffer, uploadTime };
+      } else if (isImageFile(file.filename) || isPdfFile(file.filename)) {
+        const blob = await resp.blob();
+        const dataUrl = await new Promise<string>((resolve, reject) => {
+          const reader = new FileReader();
+          reader.onload = () => resolve(reader.result as string);
+          reader.onerror = () => reject(reader.error);
+          reader.readAsDataURL(blob);
+        });
+        previewFile = { id, name: file.filename, size: blob.size, type: blob.type, preview: dataUrl, uploadTime };
+      } else {
+        // 不支持的格式回退为直接下载
+        handleDownloadFile(file.url, file.filename);
+        return;
+      }
+      emit('file-preview', previewFile);
+    } catch (err) {
+      console.error('获取文件内容失败:', err);
+      message.error('文件预览失败,请尝试下载查看');
+    }
+  };
+
   // ask_user 答案状态(按消息索引存储)
   const askUserAnswers = reactive<Record<number, string[]>>({});
   const askUserOtherSelected = reactive<Record<number, boolean[]>>({});
@@ -730,11 +789,6 @@
     return Array.from(groupMap.values());
   };
 
-  const getAgentCount = (steps: ThinkingStep[]): number => {
-    const agents = new Set(steps.map((s) => s.agent || '系统'));
-    return agents.size;
-  };
-
   const formatFullTime = (createdAt: string) => {
     return dayjs(createdAt).format('YYYY-MM-DD HH:mm:ss');
   };
@@ -1552,6 +1606,7 @@
             background: linear-gradient(135deg, rgba(0, 47, 74, 0.8) 0%, rgba(0, 60, 90, 0.6) 100%);
             border: 1px solid rgba(0, 84, 130, 0.6);
             border-radius: 10px;
+            cursor: pointer;
             transition: all 0.2s ease;
 
             &:hover {

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

@@ -6,7 +6,7 @@
     </div>
     <div class="file-list-content">
       <div v-for="file in files" :key="file.id" class="file-list-item" @click="emit('file-click', file)">
-        <div class="file-icon-small"></div>
+        <div class="file-icon-small" :style="{ backgroundImage: getFileIconVar(file.name) }"></div>
         <div class="file-info-small">
           <div class="file-name-small">{{ file.name }}</div>
           <div class="file-size-small">{{ formatFileSize(file.size) }}</div>
@@ -19,7 +19,7 @@
 
 <script setup lang="ts">
   import { CloseOutlined } from '@ant-design/icons-vue';
-  import { formatFileSize } from './utils';
+  import { formatFileSize, getFileIconVar } from './utils';
   import type { AttachedFile } from './types';
 
   defineProps<{

+ 77 - 15
src/views/ventAI/manageAssistent/components/chatModal/FilePreviewPanel.vue

@@ -1,7 +1,7 @@
 <template>
-  <div class="file-preview-panel">
-    <div class="panel-header">
-      <span class="panel-title">{{ file.name }}</span>
+  <div :class="['file-preview-panel', { 'hide-header': hideHeader }]">
+    <div v-if="!hideHeader || isEditing || editable" :class="['panel-header', { 'hide-title': hideHeader }]">
+      <span v-if="!hideHeader" class="panel-title">{{ file.name }}</span>
       <div class="panel-header-actions">
         <button v-if="editable && !isEditing" class="header-action-btn edit-btn" @click="enterEdit">
           <img :src="getCssUrl('--img-chat-edit-icon')" width="14" height="14" />
@@ -17,7 +17,7 @@
           <span>{{ saving ? '保存中...' : '保存' }}</span>
         </button>
       </div>
-      <div class="close-btn" @click="emit('close')">
+      <div v-if="!hideHeader" class="close-btn" @click="emit('close')">
         <div class="btn-close-icon"></div>
       </div>
     </div>
@@ -36,12 +36,14 @@
           <div v-else class="no-preview-text">无法加载PDF预览</div>
         </div>
         <div v-else-if="isOldDocFile(file.name)" class="no-preview">
-          <div class="no-preview-icon"></div>
+          <div class="no-preview-icon" :style="{ backgroundImage: getFileIconVar(file.name) }"></div>
           <div class="no-preview-text">暂不支持 .doc 格式预览</div>
           <div class="no-preview-hint">请将文件转换为 .docx 格式后重新上传</div>
         </div>
-        <div v-else-if="isWordFile(file.name)" class="word-preview">
-          <VueOfficeDocx v-if="file.arrayBuffer" :src="file.arrayBuffer" @rendered="handleRendered" @error="handleError" />
+        <div v-else-if="isWordFile(file.name)" ref="wordPreviewRef" class="word-preview">
+          <div v-if="file.arrayBuffer" ref="docxScaleBoxRef" class="docx-scale-box">
+            <VueOfficeDocx :src="file.arrayBuffer" @rendered="handleRendered" @error="handleError" />
+          </div>
           <div v-else class="no-preview-text">无法加载Word文档</div>
         </div>
         <div v-else-if="isExcelFile(file.name)" class="excel-preview">
@@ -89,7 +91,7 @@
           <pre>{{ file.content || '文件内容加载中...' }}</pre>
         </div>
         <div v-else class="no-preview">
-          <div class="no-preview-icon"></div>
+          <div class="no-preview-icon" :style="{ backgroundImage: getFileIconVar(file.name) }"></div>
           <div class="no-preview-text">不支持预览此文件格式</div>
           <div class="no-preview-hint">{{ getFileExtension(file.name) }} 格式文件暂不支持在线预览</div>
         </div>
@@ -99,7 +101,7 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, computed, watch } from 'vue';
+  import { ref, computed, watch, onUnmounted } from 'vue';
   import { message } from 'ant-design-vue';
   import { saveReportText } from '../../api';
   import VueOfficeDocx from '@vue-office/docx/lib/v3/vue-office-docx.mjs';
@@ -114,6 +116,7 @@
     isMdFile,
     isTextFile,
     getFileExtension,
+    getFileIconVar,
     renderMarkdown,
   } from './utils';
   import type { AttachedFile } from './types';
@@ -128,8 +131,9 @@
       file: AttachedFile;
       editable?: boolean;
       saveFilename?: string;
+      hideHeader?: boolean;
     }>(),
-    { editable: false, saveFilename: '' }
+    { editable: false, saveFilename: '', hideHeader: false }
   );
 
   const emit = defineEmits<{
@@ -262,10 +266,45 @@
       });
   });
 
+  // Word 预览按面板宽度自适应缩放
+  const wordPreviewRef = ref<HTMLElement | null>(null);
+  const docxScaleBoxRef = ref<HTMLElement | null>(null);
+  let docxNaturalWidth = 0;
+  let docxNaturalHeight = 0;
+  let docxResizeObserver: ResizeObserver | null = null;
+
+  const applyDocxScale = () => {
+    const container = wordPreviewRef.value;
+    const box = docxScaleBoxRef.value;
+    if (!container || !box || !docxNaturalWidth) return;
+    const scale = container.clientWidth / docxNaturalWidth;
+    box.style.transform = `scale(${scale})`;
+    box.style.height = `${docxNaturalHeight * scale}px`;
+  };
+
   const handleRendered = () => {
-    console.log('文件渲染完成');
+    const container = wordPreviewRef.value;
+    const box = docxScaleBoxRef.value;
+    if (!container || !box) return;
+    const sections = box.querySelectorAll<HTMLElement>('section.docx');
+    if (!sections.length) return;
+    docxNaturalWidth = sections[0].offsetWidth;
+    docxNaturalHeight = 0;
+    sections.forEach((s) => {
+      const cs = getComputedStyle(s);
+      docxNaturalHeight += s.offsetHeight + parseFloat(cs.marginTop) + parseFloat(cs.marginBottom);
+    });
+    box.style.width = `${docxNaturalWidth}px`;
+    applyDocxScale();
+    docxResizeObserver?.disconnect();
+    docxResizeObserver = new ResizeObserver(() => applyDocxScale());
+    docxResizeObserver.observe(container);
   };
 
+  onUnmounted(() => {
+    docxResizeObserver?.disconnect();
+  });
+
   const handleError = (err: any) => {
     console.error('文件预览失败:', err);
   };
@@ -276,6 +315,12 @@
     display: flex;
     flex-direction: column;
     height: 100%;
+
+    &.hide-header {
+      .file-preview-content {
+        padding-top: 12px;
+      }
+    }
   }
 
   .panel-header {
@@ -298,6 +343,14 @@
       margin-right: 10px;
     }
 
+    &.hide-title {
+      justify-content: flex-end;
+
+      .panel-header-actions {
+        margin-right: 0;
+      }
+    }
+
     .panel-header-actions {
       display: flex;
       gap: 6px;
@@ -367,8 +420,12 @@
         }
 
         @keyframes spin {
-          from { transform: rotate(0deg); }
-          to { transform: rotate(360deg); }
+          from {
+            transform: rotate(0deg);
+          }
+          to {
+            transform: rotate(360deg);
+          }
         }
       }
     }
@@ -433,13 +490,18 @@
       width: calc(100% + 32px);
       height: calc(100% + 32px);
       margin: -16px;
-      overflow: auto auto;
+      overflow: hidden auto;
+
+      .docx-scale-box {
+        display: inline-block;
+        vertical-align: top;
+        transform-origin: 0 0;
+      }
 
       :deep(.docx-wrapper) {
         display: inline-block;
         padding: 0;
         background: transparent;
-        min-width: 100%;
       }
     }
 

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

@@ -0,0 +1,458 @@
+<template>
+  <div class="right-panel right-panel-tabs">
+    <div class="panel-tab-bar">
+      <div class="panel-tab-toggle-wrapper" ref="toggleRef">
+        <div class="panel-tab-toggle" :class="{ active: showTabList }" @click="toggleTabList" title="全部标签页">
+          <div class="panel-tab-toggle-icon"></div>
+        </div>
+        <div v-if="showTabList" class="tab-list-popup" @click.stop @pointerdown.stop>
+          <div class="tab-list-search">
+            <div class="tab-list-search-icon"></div>
+            <input ref="searchInputRef" v-model="searchText" class="tab-list-search-input" placeholder="搜索标签页" />
+          </div>
+          <div class="tab-list">
+            <div
+              v-for="tab in filteredTabs"
+              :key="tab.id"
+              class="tab-list-item"
+              :class="{ active: tab.id === activeTabId }"
+              @click="handleSelect(tab.id)"
+            >
+              <div
+                class="tab-list-item-icon"
+                :class="`tab-icon-${tab.type}`"
+                :style="tab.type === 'file' ? { backgroundImage: getFileIconVar(tab.title) } : null"
+              ></div>
+              <span class="tab-list-item-title">{{ tab.title }}</span>
+            </div>
+            <div v-if="filteredTabs.length === 0" class="tab-list-empty">暂无匹配标签页</div>
+          </div>
+        </div>
+      </div>
+      <div class="panel-tab-scroll" ref="scrollRef" :class="{ sliding: isSliding }">
+        <div class="panel-tab-track" :style="isSliding ? { transform: `translateX(${-trackShiftPx}px)` } : null">
+          <div
+            v-for="tab in tabs"
+            :key="tab.id"
+            :class="['panel-tab', `panel-tab-${tab.type}`, { active: tab.id === activeTabId }]"
+            @click="handleSwitch(tab.id)"
+          >
+            <div class="panel-tab-icon" :style="tab.type === 'file' ? { backgroundImage: getFileIconVar(tab.title) } : null"></div>
+            <span class="panel-tab-title">{{ tab.title }}</span>
+            <div class="panel-tab-close" @click.stop="handleClose(tab.id)" title="关闭"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="panel-tab-content">
+      <FilePreviewPanel
+        v-if="activeTab?.type === 'file'"
+        :file="activeTab.file"
+        :editable="activeTab.editable"
+        :save-filename="activeTab.file.name || ''"
+        hide-header
+        @close="closeActive"
+        @save-success="handleSaveSuccess"
+      />
+      <AgentDetailPanel
+        v-else-if="activeTab?.type === 'agent'"
+        :messageIndex="activeTab.messageIndex"
+        :agentName="activeTab.agent"
+        :agentId="activeTab.agentId"
+        :messages="messages"
+        hide-header
+        @close="closeActive"
+      />
+      <ScheduleDetailPanel
+        v-else-if="activeTab?.type === 'schedule'"
+        :task="activeTab.task"
+        hide-header
+        @close="closeActive"
+        @deleted="emit('schedule-deleted')"
+        @updated="emit('schedule-updated')"
+        @open-session="handleOpenSession"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, nextTick, ref } from 'vue';
+  import { onClickOutside, useResizeObserver } from '@vueuse/core';
+  import FilePreviewPanel from './FilePreviewPanel.vue';
+  import AgentDetailPanel from './AgentDetailPanel.vue';
+  import ScheduleDetailPanel from './ScheduleDetailPanel.vue';
+  import { getFileIconVar } from './utils';
+  import type { Message, RightPanelTab } from './types';
+
+  const props = defineProps<{
+    tabs: RightPanelTab[];
+    activeTabId: string | null;
+    messages: Message[];
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'switch-tab', id: string): void;
+    (e: 'close-tab', id: string): void;
+    (e: 'save-success', content: string): void;
+    (e: 'schedule-deleted'): void;
+    (e: 'schedule-updated'): void;
+    (e: 'open-schedule-session', sessionId: string, prompt: string): void;
+  }>();
+
+  const activeTab = computed(() => props.tabs.find((t) => t.id === props.activeTabId) || null);
+
+  // 按容器实际宽度动态计算可容纳的标签数量;激活标签像素中心对齐容器中心,平移量钳制保证首标签贴左、尾标签贴右
+  const TAB_GAP = 8;
+  const TAB_BASE_WIDTH = 150;
+  const scrollRef = ref<HTMLElement>();
+  const visibleCount = ref(1);
+  const scrollWidth = ref(0);
+
+  useResizeObserver(scrollRef, () => {
+    const width = scrollRef.value?.clientWidth ?? 0;
+    scrollWidth.value = width;
+    visibleCount.value = Math.max(1, Math.floor((width + TAB_GAP) / (TAB_BASE_WIDTH + TAB_GAP)));
+  });
+
+  const trackShiftPx = computed(() => {
+    const len = props.tabs.length;
+    if (len <= visibleCount.value || !scrollWidth.value) return 0;
+    const trackWidth = len * (TAB_BASE_WIDTH + TAB_GAP) - TAB_GAP;
+    const maxShift = Math.max(0, trackWidth - scrollWidth.value);
+    const activeIdx = Math.max(
+      0,
+      props.tabs.findIndex((t) => t.id === props.activeTabId)
+    );
+    const activeCenter = activeIdx * (TAB_BASE_WIDTH + TAB_GAP) + TAB_BASE_WIDTH / 2;
+    return Math.min(Math.max(activeCenter - scrollWidth.value / 2, 0), maxShift);
+  });
+
+  const isSliding = computed(() => props.tabs.length > visibleCount.value);
+
+  const toggleRef = ref<HTMLElement>();
+  const searchInputRef = ref<HTMLInputElement>();
+  const showTabList = ref(false);
+  const searchText = ref('');
+
+  const filteredTabs = computed(() => {
+    const kw = searchText.value.trim().toLowerCase();
+    if (!kw) return props.tabs;
+    return props.tabs.filter((t) => t.title.toLowerCase().includes(kw));
+  });
+
+  const toggleTabList = async () => {
+    showTabList.value = !showTabList.value;
+    if (showTabList.value) {
+      searchText.value = '';
+      await nextTick();
+      searchInputRef.value?.focus();
+    }
+  };
+
+  const handleSelect = (id: string) => {
+    emit('switch-tab', id);
+    showTabList.value = false;
+  };
+
+  onClickOutside(toggleRef, () => {
+    showTabList.value = false;
+  });
+
+  const handleSwitch = (id: string) => emit('switch-tab', id);
+  const handleClose = (id: string) => emit('close-tab', id);
+  const closeActive = () => {
+    if (activeTab.value) emit('close-tab', activeTab.value.id);
+  };
+  const handleSaveSuccess = (content: string) => emit('save-success', content);
+  const handleOpenSession = (sessionId: string, prompt: string) => emit('open-schedule-session', sessionId, prompt);
+</script>
+
+<style scoped lang="less">
+  .right-panel-tabs {
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+
+    .panel-tab-bar {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 10px 12px;
+      flex-shrink: 0;
+      border-bottom: 1px solid rgba(63, 80, 106, 0.25);
+
+      .panel-tab-toggle-wrapper {
+        position: relative;
+        flex-shrink: 0;
+        display: flex;
+        align-items: center;
+
+        .panel-tab-toggle {
+          width: 28px;
+          height: 28px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          border-radius: 4px;
+          border: 1px solid rgba(63, 80, 106, 0.35);
+          background: rgba(63, 80, 106, 0.12);
+          cursor: pointer;
+          transition: all 0.2s;
+
+          &:hover,
+          &.active {
+            background: rgba(10, 132, 255, 0.15);
+            border-color: rgba(10, 132, 255, 0.4);
+          }
+
+          .panel-tab-toggle-icon {
+            width: 14px;
+            height: 14px;
+            background-image: var(--img-chat-tabs-icon);
+            background-repeat: no-repeat;
+            background-size: 100% 100%;
+            background-position: center;
+            opacity: 0.8;
+          }
+        }
+
+        .tab-list-popup {
+          position: absolute;
+          top: calc(100% + 6px);
+          left: 0;
+          width: 240px;
+          max-height: 320px;
+          display: flex;
+          flex-direction: column;
+          background: #0a2a3f;
+          border: 1px solid #1a4a6f;
+          border-radius: 8px;
+          box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+          z-index: 1000;
+
+          .tab-list-search {
+            margin: 10px 10px 4px;
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            height: 34px;
+            padding: 0 12px;
+            border: none;
+            border-bottom: 1px solid rgba(63, 80, 106, 0.5);
+            background: transparent;
+            flex-shrink: 0;
+            transition: border-bottom-color 0.2s;
+
+            &:hover {
+              border-bottom-color: rgba(63, 80, 106, 0.7);
+            }
+
+            &:focus-within {
+              border-bottom-color: rgba(10, 132, 255, 0.7);
+            }
+
+            .tab-list-search-icon {
+              width: 15px;
+              height: 15px;
+              flex-shrink: 0;
+              background-image: var(--img-chat-options-search-icon);
+              background-repeat: no-repeat;
+              background-size: 100% 100%;
+              background-position: center;
+              opacity: 0.7;
+            }
+
+            .tab-list-search-input {
+              flex: 1;
+              min-width: 0;
+              height: 100%;
+              border: none;
+              padding: 0;
+              background: transparent;
+              color: #e0e6ed;
+              font-size: 12px;
+              outline: none;
+
+              &::placeholder {
+                color: #6b7684;
+              }
+            }
+          }
+
+          .tab-list {
+            flex: 1;
+            overflow-y: auto;
+            padding: 6px;
+
+            .tab-list-item {
+              display: flex;
+              align-items: center;
+              gap: 10px;
+              padding: 8px 10px;
+              cursor: pointer;
+              border-radius: 6px;
+              transition: background 0.2s;
+
+              &:hover {
+                background: rgba(10, 132, 255, 0.15);
+              }
+
+              &.active {
+                background: rgba(10, 132, 255, 0.2);
+              }
+
+              .tab-list-item-icon {
+                width: 14px;
+                height: 14px;
+                flex-shrink: 0;
+                background-repeat: no-repeat;
+                background-size: 100% 100%;
+                background-position: center;
+                opacity: 0.8;
+              }
+
+              .tab-icon-file {
+                background-image: var(--img-chat-attach-icon);
+              }
+
+              .tab-icon-agent {
+                background-image: var(--img-chat-agent-icon);
+              }
+
+              .tab-icon-schedule {
+                background-image: var(--img-chat-options-schedule-icon);
+              }
+
+              .tab-list-item-title {
+                flex: 1;
+                min-width: 0;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                font-size: 12px;
+                color: #c9d1d9;
+              }
+            }
+
+            .tab-list-empty {
+              padding: 16px 0;
+              text-align: center;
+              font-size: 12px;
+              color: #6b7684;
+            }
+          }
+        }
+      }
+
+      .panel-tab-scroll {
+        flex: 1;
+        min-width: 0;
+        overflow: hidden;
+
+        .panel-tab-track {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          width: 100%;
+        }
+
+        &.sliding .panel-tab-track {
+          width: max-content;
+          transition: transform 0.25s ease;
+        }
+
+        &:not(.sliding) .panel-tab {
+          flex: 1 1 150px;
+          max-width: 240px;
+        }
+      }
+
+      .panel-tab {
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        height: 28px;
+        flex: 0 0 150px;
+        min-width: 0;
+        padding: 6px 12px;
+        border-radius: 4px;
+        border: 1px solid rgba(63, 80, 106, 0.35);
+        background: rgba(63, 80, 106, 0.12);
+        cursor: pointer;
+        white-space: nowrap;
+        font-size: 12px;
+        color: #8b949e;
+        transition: all 0.2s;
+
+        &:hover {
+          background: rgba(10, 132, 255, 0.08);
+          border-color: rgba(10, 132, 255, 0.3);
+          color: #c9d1d9;
+        }
+
+        &.active {
+          background: rgba(10, 132, 255, 0.18);
+          border-color: rgba(10, 132, 255, 0.5);
+          color: #e0e6ed;
+          box-shadow: 0 1px 4px rgba(10, 132, 255, 0.25);
+        }
+
+        .panel-tab-icon {
+          width: 14px;
+          height: 14px;
+          flex-shrink: 0;
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          background-position: center;
+          opacity: 0.8;
+        }
+
+        .panel-tab-title {
+          flex: 1;
+          min-width: 0;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+
+        .panel-tab-close {
+          width: 16px;
+          height: 16px;
+          flex-shrink: 0;
+          background-image: var(--img-chat-close-icon-btn);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          background-position: center;
+          opacity: 0.6;
+          border-radius: 3px;
+          transition: all 0.2s;
+
+          &:hover {
+            opacity: 1;
+            background-color: rgba(255, 77, 79, 0.2);
+          }
+        }
+      }
+
+      .panel-tab-file .panel-tab-icon {
+        background-image: var(--img-chat-attach-icon);
+      }
+
+      .panel-tab-agent .panel-tab-icon {
+        background-image: var(--img-chat-agent-icon);
+      }
+
+      .panel-tab-schedule .panel-tab-icon {
+        background-image: var(--img-chat-options-schedule-icon);
+      }
+    }
+
+    .panel-tab-content {
+      flex: 1;
+      min-height: 0;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+    }
+  }
+</style>

+ 7 - 18
src/views/ventAI/manageAssistent/components/chatModal/ScheduleDetailPanel.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="schedule-detail-panel">
-    <div class="schedule-detail-header">
+    <div v-if="!hideHeader" class="schedule-detail-header">
       <span class="schedule-detail-title">定时任务</span>
       <div class="schedule-detail-close" @click="emit('close')">
         <div class="close-icon"></div>
@@ -86,25 +86,14 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, reactive, watch } from 'vue';
+  import { ref, watch } from 'vue';
   import { onClickOutside } from '@vueuse/core';
   import { message, Modal } from 'ant-design-vue';
   import { getScheduleRuns, pauseSchedule, resumeSchedule, deleteSchedule } from '../../api';
   import CreateScheduleModal from './CreateScheduleModal.vue';
+  import type { ScheduleTask } from './types';
 
-  interface ScheduleTask {
-    id: number;
-    name: string;
-    cron_expr: string;
-    cron_human: string;
-    prompt: string;
-    enabled: boolean;
-    next_run_at: string | null;
-    feedback_session_id?: string;
-    schedule?: { freq: string; human?: string };
-  }
-
-  const props = defineProps<{ task: ScheduleTask | null }>();
+  const props = defineProps<{ task: ScheduleTask | null; hideHeader?: boolean }>();
   const emit = defineEmits<{
     (e: 'close'): void;
     (e: 'deleted'): void;
@@ -248,9 +237,9 @@
     display: flex;
     flex-direction: column;
     overflow: hidden;
-    background-image: var(--img-chat-panel-bg);
-    background-repeat: no-repeat;
-    background-size: 100% 100%;
+    // background-image: var(--img-chat-panel-bg);
+    // background-repeat: no-repeat;
+    // background-size: 100% 100%;
     border-radius: 4px;
   }
 

+ 24 - 0
src/views/ventAI/manageAssistent/components/chatModal/types.ts

@@ -103,6 +103,28 @@ export interface Message {
   generateStartTime?: number;
 }
 
+export interface ScheduleTask {
+  id: number;
+  name: string;
+  cron_expr: string;
+  cron_human: string;
+  prompt: string;
+  enabled: boolean;
+  next_run_at: string | null;
+  feedback_session_id?: string;
+  schedule?: { freq: string; human?: string };
+}
+
+export type RightPanelTab =
+  | { id: string; type: 'file'; title: string; file: AttachedFile; editable: boolean }
+  | { id: string; type: 'agent'; title: string; messageIndex: number; agent: string; agentId?: string }
+  | { id: string; type: 'schedule'; title: string; task: ScheduleTask };
+
+export interface RightPanelState {
+  tabs: RightPanelTab[];
+  activeTabId: string | null;
+}
+
 export interface Task {
   id: string;
   name: string;
@@ -115,6 +137,8 @@ export interface Task {
   isStreaming?: boolean;
   // 流式期间从会话列表归并得到的后端会话 id(流结束前 sessionId 尚未返回)
   pendingSessionId?: string;
+  // 右侧辅助面板标签页状态(按任务维度保存)
+  rightPanels?: RightPanelState;
 }
 
 export type SectionType = 'select' | 'params' | 'result';

+ 12 - 2
src/views/ventAI/manageAssistent/components/chatModal/utils.ts

@@ -79,10 +79,10 @@ export const wrapTables = (html: string, icons: { copy: string; download: string
         <span class="table-action-btn" data-action="copy" data-table="${encodedTable}" data-icon="${icons.copy}" data-icon-success="${icons.copySuccess}" title="复制Markdown">
           <img src="${icons.copy}" width="14" height="14" />
         </span>
-        <span class="table-action-btn" data-action="csv" data-table="${encodedTable}" title="下载CSV">
+        <span class="table-action-btn" data-action="csv" data-table="${encodedTable}" title="下载">
           <img src="${icons.download}" width="14" height="14" />
         </span>
-        <span class="table-action-btn" data-action="preview" data-table="${encodedTable}" title="预览表格">
+        <span class="table-action-btn" data-action="preview" data-table="${encodedTable}" title="预览">
           <img src="${icons.preview}" width="14" height="14" />
         </span>
       </div>
@@ -175,3 +175,13 @@ export const getFileExtension = (filename: string): string => {
   const match = filename.match(/\.([^.]+)$/);
   return match ? `.${match[1]}`.toUpperCase() : '';
 };
+
+export const getFileIconVar = (filename: string): string => {
+  if (isMdFile(filename)) return 'var(--img-chat-file-md-icon)';
+  if (isPdfFile(filename)) return 'var(--img-chat-file-pdf-icon)';
+  if (isWordFile(filename)) return 'var(--img-chat-file-word-icon)';
+  if (isExcelFile(filename) || isCsvFile(filename)) return 'var(--img-chat-file-excel-icon)';
+  if (isImageFile(filename)) return 'var(--img-chat-file-img-icon)';
+  if (isTextFile(filename)) return 'var(--img-chat-file-txt-icon)';
+  return 'var(--img-chat-attach-icon)';
+};

部分文件因为文件数量过多而无法显示