소스 검색

[Mod 0000]AI助手 聊天框标题内容组件封装

wangkeyi 1 주 전
부모
커밋
f1de45ebaf
2개의 변경된 파일240개의 추가작업 그리고 188개의 파일을 삭제
  1. 15 188
      src/views/ventAI/manageAssistent/components/AiAssistantModal.vue
  2. 225 0
      src/views/ventAI/manageAssistent/components/chatModal/ChatHeader.vue

+ 15 - 188
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -39,31 +39,18 @@
       <template v-if="activePanel === 'chat'">
         <div class="chat-panel">
           <!-- 顶部任务选择器 -->
-          <div class="chat-header">
-            <template v-if="panelCollapsed">
-              <div :class="['nav-btn', { disabled: !canGoPrev }]" @click="goToPrev">
-                <div class="nav-icon prev-icon"></div>
-              </div>
-              <div :class="['nav-btn', { disabled: !canGoNext }]" @click="goToNext">
-                <div class="nav-icon next-icon"></div>
-              </div>
-              <div class="nav-btn" @click="createNewTask">
-                <div class="nav-icon add-icon"></div>
-              </div>
-            </template>
-            <div v-if="currentTask?.sessionId" class="chat-header-title-wrapper">
-              <template v-if="editingTitle">
-                <input v-model="editTitleValue" class="title-edit-input" @keyup.enter="saveTitle" @keyup.escape="cancelEditTitle" @blur="saveTitle" />
-              </template>
-              <template v-else>
-                <div class="title-edit-btn" @click="startEditTitle" title="编辑标题"></div>
-                <span class="chat-header-title">{{ currentTask.name }}</span>
-              </template>
-            </div>
-            <div v-if="currentWordUrl" class="upload-bg" @click="downloadWordReport">
-              <div class="download-icon"></div>
-            </div>
-          </div>
+          <ChatHeader
+            :currentTask="currentTask"
+            :panelCollapsed="panelCollapsed"
+            :canGoPrev="canGoPrev"
+            :canGoNext="canGoNext"
+            :currentWordUrl="currentWordUrl"
+            @go-to-prev="goToPrev"
+            @go-to-next="goToNext"
+            @create-task="createNewTask"
+            @save-title="handleSaveTitle"
+            @download-word="downloadWordReport"
+          />
 
           <!-- 附件列表面板 -->
           <FileListPanel :visible="showFileList" :files="getCurrentTaskFiles()" @close="showFileList = false" @file-click="openFilePreview" />
@@ -193,6 +180,7 @@
   } from '../api';
   import { message, Modal } from 'ant-design-vue';
   import TaskListPanel from './chatModal/TaskListPanel.vue';
+  import ChatHeader from './chatModal/ChatHeader.vue';
   import FileListPanel from './chatModal/FileListPanel.vue';
   import ChatMessages from './chatModal/ChatMessages.vue';
   import type { AgentDetailData } from './chatModal/ChatMessages.vue';
@@ -406,26 +394,9 @@
     schedulePanelRef.value?.fetchScheduleList?.();
   };
 
-  const editingTitle = ref(false);
-  const editTitleValue = ref('');
-
-  const startEditTitle = () => {
+  // 标题保存(由 ChatHeader 子组件触发,父组件负责 API 调用和状态更新)
+  const handleSaveTitle = async (newTitle: string) => {
     if (!currentTask.value?.sessionId) return;
-    editTitleValue.value = currentTask.value.name;
-    editingTitle.value = true;
-    nextTick(() => {
-      const input = document.querySelector('.title-edit-input') as HTMLInputElement;
-      input?.focus();
-      input?.select();
-    });
-  };
-
-  const saveTitle = async () => {
-    const newTitle = editTitleValue.value.trim();
-    if (!currentTask.value?.sessionId || !newTitle || newTitle === currentTask.value.name) {
-      editingTitle.value = false;
-      return;
-    }
     try {
       await editSessionTitle(currentTask.value.sessionId, newTitle);
       currentTask.value.name = newTitle;
@@ -433,11 +404,6 @@
     } catch {
       message.error('标题修改失败');
     }
-    editingTitle.value = false;
-  };
-
-  const cancelEditTitle = () => {
-    editingTitle.value = false;
   };
 
   const isFullscreen = ref(false);
@@ -2060,145 +2026,6 @@
     border-radius: 4px;
     overflow: hidden;
     min-width: 0;
-
-    :deep(.chat-header) {
-      display: flex;
-      padding: 12px 20px;
-      flex-shrink: 0;
-      align-items: center;
-      background: rgba(2, 53, 100, 0.3);
-
-      .header-icon {
-        width: 35px;
-        height: 28px;
-        background-image: var(--img-chat-header-icon);
-        background-repeat: no-repeat;
-        background-size: 100% 100%;
-        margin-right: 10px;
-      }
-
-      .chat-header-title {
-        font-size: 16px;
-        color: #e0e6ed;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
-        margin-left: 10px;
-      }
-
-      .chat-header-title-wrapper {
-        display: flex;
-        align-items: center;
-        flex: 1;
-        min-width: 0;
-
-        &:hover .title-edit-btn {
-          opacity: 1;
-        }
-
-        .title-edit-btn {
-          width: 28px;
-          height: 28px;
-          flex-shrink: 0;
-          background-image: var(--img-chat-edit-icon);
-          background-repeat: no-repeat;
-          background-size: 16px 16px;
-          background-position: center;
-          cursor: pointer;
-          opacity: 0;
-          transition: opacity 0.2s;
-          border-radius: 4px;
-          margin-right: 4px;
-
-          &:hover {
-            background-color: rgba(10, 132, 255, 0.2);
-          }
-        }
-
-        .chat-header-title {
-          margin-left: 0;
-          flex: 1;
-          min-width: 0;
-        }
-
-        .title-edit-input {
-          flex: 1;
-          min-width: 0;
-          height: 28px;
-          padding: 0 8px;
-          font-size: 14px;
-          color: #e0e6ed;
-          background: rgba(10, 132, 255, 0.1);
-          border: 1px solid rgba(10, 132, 255, 0.5);
-          border-radius: 4px;
-          outline: none;
-
-          &:focus {
-            border-color: rgba(10, 132, 255, 0.8);
-          }
-        }
-      }
-
-      .nav-btn {
-        width: 28px;
-        height: 28px;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        cursor: pointer;
-        border-radius: 4px;
-        transition: background 0.2s;
-
-        &:hover:not(.disabled) {
-          background: rgba(10, 132, 255, 0.15);
-        }
-
-        &.disabled {
-          opacity: 0.3;
-          cursor: not-allowed;
-        }
-
-        .nav-icon {
-          width: 16px;
-          height: 16px;
-          background-repeat: no-repeat;
-          background-size: 100% 100%;
-        }
-
-        .prev-icon {
-          background-image: var(--img-chat-prev-icon);
-        }
-
-        .next-icon {
-          background-image: var(--img-chat-next-icon);
-        }
-
-        .add-icon {
-          background-image: var(--img-chat-options-add-icon);
-        }
-      }
-
-      .upload-bg {
-        cursor: pointer;
-        width: 36px;
-        height: 36px;
-        background-image: var(--img-chat-close-bg);
-        background-repeat: no-repeat;
-        background-size: 100% 100%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        margin-left: auto;
-        position: relative;
-      }
-      .download-icon {
-        width: 20px;
-        height: 20px;
-        background-image: var(--img-chat-download-icon);
-        background-repeat: no-repeat;
-        background-size: 100% 100%;
-      }
-    }
   }
 
   .right-panel {

+ 225 - 0
src/views/ventAI/manageAssistent/components/chatModal/ChatHeader.vue

@@ -0,0 +1,225 @@
+<template>
+  <!-- 顶部任务选择器 -->
+  <div class="chat-header">
+    <!-- 任务列表收起时显示导航按钮 -->
+    <template v-if="panelCollapsed">
+      <div :class="['nav-btn', { disabled: !canGoPrev }]" @click="emit('go-to-prev')">
+        <div class="nav-icon prev-icon"></div>
+      </div>
+      <div :class="['nav-btn', { disabled: !canGoNext }]" @click="emit('go-to-next')">
+        <div class="nav-icon next-icon"></div>
+      </div>
+      <div class="nav-btn" @click="emit('create-task')">
+        <div class="nav-icon add-icon"></div>
+      </div>
+    </template>
+
+    <!-- 会话标题区域(含编辑功能) -->
+    <div v-if="currentTask?.sessionId" class="chat-header-title-wrapper">
+      <template v-if="editingTitle">
+        <input
+          ref="titleInputRef"
+          v-model="editTitleValue"
+          class="title-edit-input"
+          @keyup.enter="handleSaveTitle"
+          @keyup.escape="cancelEditTitle"
+          @blur="handleSaveTitle"
+        />
+      </template>
+      <template v-else>
+        <div class="title-edit-btn" @click="startEditTitle" title="编辑标题"></div>
+        <span class="chat-header-title">{{ currentTask.name }}</span>
+      </template>
+    </div>
+
+    <!-- Word 报告下载按钮 -->
+    <div v-if="currentWordUrl" class="upload-bg" @click="emit('download-word')">
+      <div class="download-icon"></div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, nextTick } from 'vue';
+  import type { Task } from './types';
+
+  interface Props {
+    currentTask: Task | undefined;
+    panelCollapsed: boolean;
+    canGoPrev: boolean;
+    canGoNext: boolean;
+    currentWordUrl: string;
+  }
+
+  const props = defineProps<Props>();
+
+  const emit = defineEmits<{
+    'go-to-prev': [];
+    'go-to-next': [];
+    'create-task': [];
+    // 保存标题时触发,参数为新标题(父组件负责 API 调用和状态更新)
+    'save-title': [newTitle: string];
+    'download-word': [];
+  }>();
+
+  // 标题编辑状态(UI 交互逻辑,内聚在子组件中)
+  const editingTitle = ref(false);
+  const editTitleValue = ref('');
+  const titleInputRef = ref<HTMLInputElement | null>(null);
+
+  const startEditTitle = () => {
+    if (!props.currentTask?.sessionId) return;
+    editTitleValue.value = props.currentTask.name;
+    editingTitle.value = true;
+    nextTick(() => {
+      titleInputRef.value?.focus();
+      titleInputRef.value?.select();
+    });
+  };
+
+  const handleSaveTitle = () => {
+    const newTitle = editTitleValue.value.trim();
+    editingTitle.value = false;
+    // 标题有变化时通知父组件保存
+    if (props.currentTask?.sessionId && newTitle && newTitle !== props.currentTask.name) {
+      emit('save-title', newTitle);
+    }
+  };
+
+  const cancelEditTitle = () => {
+    editingTitle.value = false;
+  };
+</script>
+
+<style scoped lang="less">
+  .chat-header {
+    display: flex;
+    padding: 12px 20px;
+    flex-shrink: 0;
+    align-items: center;
+    background: rgba(2, 53, 100, 0.3);
+
+    .chat-header-title {
+      font-size: 16px;
+      color: #e0e6ed;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      margin-left: 10px;
+    }
+
+    .chat-header-title-wrapper {
+      display: flex;
+      align-items: center;
+      flex: 1;
+      min-width: 0;
+
+      &:hover .title-edit-btn {
+        opacity: 1;
+      }
+
+      .title-edit-btn {
+        width: 28px;
+        height: 28px;
+        flex-shrink: 0;
+        background-image: var(--img-chat-edit-icon);
+        background-repeat: no-repeat;
+        background-size: 16px 16px;
+        background-position: center;
+        cursor: pointer;
+        opacity: 0;
+        transition: opacity 0.2s;
+        border-radius: 4px;
+        margin-right: 4px;
+
+        &:hover {
+          background-color: rgba(10, 132, 255, 0.2);
+        }
+      }
+
+      .chat-header-title {
+        margin-left: 0;
+        flex: 1;
+        min-width: 0;
+      }
+
+      .title-edit-input {
+        flex: 1;
+        min-width: 0;
+        height: 28px;
+        padding: 0 8px;
+        font-size: 14px;
+        color: #e0e6ed;
+        background: rgba(10, 132, 255, 0.1);
+        border: 1px solid rgba(10, 132, 255, 0.5);
+        border-radius: 4px;
+        outline: none;
+
+        &:focus {
+          border-color: rgba(10, 132, 255, 0.8);
+        }
+      }
+    }
+
+    .nav-btn {
+      width: 28px;
+      height: 28px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      border-radius: 4px;
+      transition: background 0.2s;
+
+      &:hover:not(.disabled) {
+        background: rgba(10, 132, 255, 0.15);
+      }
+
+      &.disabled {
+        opacity: 0.3;
+        cursor: not-allowed;
+      }
+
+      .nav-icon {
+        width: 16px;
+        height: 16px;
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+
+      .prev-icon {
+        background-image: var(--img-chat-prev-icon);
+      }
+
+      .next-icon {
+        background-image: var(--img-chat-next-icon);
+      }
+
+      .add-icon {
+        background-image: var(--img-chat-options-add-icon);
+      }
+    }
+
+    .upload-bg {
+      cursor: pointer;
+      width: 36px;
+      height: 36px;
+      background-image: var(--img-chat-close-bg);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-left: auto;
+      position: relative;
+    }
+
+    .download-icon {
+      width: 20px;
+      height: 20px;
+      background-image: var(--img-chat-download-icon);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+    }
+  }
+</style>