Przeglądaj źródła

[Mod 0000]将AI弹框和主页放入自适应容器

wangkeyi 1 tydzień temu
rodzic
commit
c922957358

+ 7 - 1
src/layouts/default/index.vue

@@ -24,7 +24,7 @@
     </div>
     </div>
 
 
     <!-- AI助手弹窗 -->
     <!-- AI助手弹窗 -->
-    <AiAssistantModal :visible="aiAssistantVisible" @close="aiAssistantVisible = false" />
+    <AiAssistantModal :visible="aiAssistantVisible" :get-container="getAiModalContainer" @close="aiAssistantVisible = false" />
   </Layout>
   </Layout>
 </template>
 </template>
 
 
@@ -86,6 +86,11 @@
         aiAssistantVisible.value = true;
         aiAssistantVisible.value = true;
       };
       };
 
 
+      // AI 弹框渲染进 adaptive-container,与页面同坐标系、同缩放;最大化后即与 AI 首页(homeAI)形态一致
+      function getAiModalContainer() {
+        return document.getElementById('adaptive-container') || document.body;
+      }
+
       // 拖动辅助函数
       // 拖动辅助函数
       const BTN_SIZE = 80;
       const BTN_SIZE = 80;
       function getLayoutHeight() {
       function getLayoutHeight() {
@@ -178,6 +183,7 @@
         aiAssistantVisible,
         aiAssistantVisible,
         aiBtnPosition,
         aiBtnPosition,
         openAiAssistant,
         openAiAssistant,
+        getAiModalContainer,
         handleDragStart,
         handleDragStart,
       };
       };
     },
     },

+ 17 - 54
src/views/vent/home/homeAI/index.vue

@@ -1,46 +1,28 @@
 <!-- eslint-disable vue/multi-word-component-names -->
 <!-- eslint-disable vue/multi-word-component-names -->
 <template>
 <template>
   <div class="home-ai-page">
   <div class="home-ai-page">
-    <AiAssistantModal :visible="visible" default-fullscreen :show-controls="false" />
-    <Teleport to="body">
-      <div class="home-menu-layer" :style="menuLayerStyle">
-        <BottomSider />
-        <div class="user-dropdown-wrap">
-          <UserDropDown :theme="getHeaderTheme" />
-        </div>
-      </div>
-    </Teleport>
+    <AiAssistantModal :visible="visible" default-fullscreen :show-controls="false" :get-container="getAiContainer" />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-  import { computed, onMounted, ref } from 'vue';
+  import { onMounted, onUnmounted, ref } from 'vue';
   import AiAssistantModal from '/@/views/ventAI/manageAssistent/components/AiAssistantModal.vue';
   import AiAssistantModal from '/@/views/ventAI/manageAssistent/components/AiAssistantModal.vue';
-  import BottomSider from '/@/layouts/default/sider/bottomSideder.vue';
-  import UserDropDown from '/@/layouts/default/header/components/user-dropdown/index.vue';
-  import { useAppStore } from '/@/store/modules/app';
-  import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
-
-  const { getHeaderTheme } = useHeaderSetting();
-
   const visible = ref(false);
   const visible = ref(false);
-  const appStore = useAppStore();
+
+  // AI 弹框渲染进 #adaptive-container,与页面同坐标系、同缩放:
+  // 布局层的 BottomSider 与本页右上用户下拉(z-index 均高于弹框)才能正常盖在弹框之上
+  const getAiContainer = () => document.getElementById('adaptive-container') || document.body;
 
 
   onMounted(() => {
   onMounted(() => {
+    // 标记 AI 首页:隐藏布局层右侧操作栏(天气/语音/模式切换等),仅保留本页内嵌的右上用户下拉
+    document.body.classList.add('ai-home-page');
     // 初始 false 再置 true,触发 AiAssistantModal 的 visible watcher 完成会话列表等初始化加载
     // 初始 false 再置 true,触发 AiAssistantModal 的 visible watcher 完成会话列表等初始化加载
     visible.value = true;
     visible.value = true;
   });
   });
 
 
-  // 布局层被 #adaptive-container 的 transform 层叠上下文锁住,
-  // 无法盖过挂在 body 层的全屏 AI 弹框;将菜单和右上操作栏 teleport 到 body,
-  // 并用与容器一致的尺寸+缩放复刻坐标系,使其显示在弹框之上
-  const menuLayerStyle = computed(() => {
-    const container = document.getElementById('adaptive-container');
-    return {
-      width: `${container?.clientWidth || 1920}px`,
-      height: `${container?.clientHeight || 928}px`,
-      transform: `scale(${appStore.getWidthScale}, ${appStore.getHeightScale})`,
-    };
+  onUnmounted(() => {
+    document.body.classList.remove('ai-home-page');
   });
   });
 </script>
 </script>
 
 
@@ -50,32 +32,13 @@
     height: 100%;
     height: 100%;
   }
   }
 
 
-  .home-menu-layer {
-    position: fixed;
-    top: 0;
-    left: 0;
-    z-index: 9999999;
-    transform-origin: left top;
-    pointer-events: none;
 
 
-    > * {
-      pointer-events: auto;
-    }
-  }
-
-  // 右上角用户下拉:fixed 定位复刻原 RightPositionBar 容器(top 44 / right 20),
-  // 并重置 UserDropDown 根节点右侧 padding 以贴齐右边缘
-  .user-dropdown-wrap {
-    position: fixed;
-    top: 16px;
-    right: 48px;
-    z-index: 999999;
-    height: 48px;
-    display: flex;
-    align-items: center;
+</style>
 
 
-    :deep(.jeecg-header-user-dropdown) {
-      padding: 0 16px 0 0;
-    }
+<!-- <style lang="less">
+  // 全局(非 scoped):AI 首页隐藏布局层 header 的右侧操作栏,改由本页内嵌的右上用户下拉替代,
+  // 避免与布局层重复渲染天气/语音/模式切换及用户下拉等控件
+  body.ai-home-page .jeecg-layout-header-action {
+    display: none !important;
   }
   }
-</style>
+</style> -->

+ 52 - 31
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -7,6 +7,7 @@
     destroyOnClose
     destroyOnClose
     :closable="showControls"
     :closable="showControls"
     :zIndex="props.zIndex"
     :zIndex="props.zIndex"
+    :get-container="getModalContainer"
     :class="['ai-assistant-modal', { fullscreen: isFullscreen }]"
     :class="['ai-assistant-modal', { fullscreen: isFullscreen }]"
   >
   >
     <template #title>
     <template #title>
@@ -192,6 +193,8 @@
     // 是否显示窗口控制按钮(最大化/还原、关闭)
     // 是否显示窗口控制按钮(最大化/还原、关闭)
     showControls?: boolean;
     showControls?: boolean;
     zIndex?: number;
     zIndex?: number;
+    // 挂载容器:不传则默认挂到 body;首页等场景传入 adaptive-container,使其与页面同坐标系、同缩放
+    getContainer?: () => HTMLElement;
   }
   }
 
 
   interface Emits {
   interface Emits {
@@ -204,6 +207,20 @@
   });
   });
   const emit = defineEmits<Emits>();
   const emit = defineEmits<Emits>();
 
 
+  // 弹框真正渲染/被 antd 滚动锁锁定的目标:包一层 size-0 空壳,
+  // 避免 antd Modal 打开时滚动锁直接改写 #adaptive-container 的宽度(导致整页缩放错乱、露出白边)
+  const portalWrapper = ref<HTMLDivElement | null>(null);
+
+  const getModalContainer = (): HTMLElement => {
+    if (portalWrapper.value) return portalWrapper.value;
+    const target = props.getContainer ? props.getContainer() : document.body;
+    const wrapper = document.createElement('div');
+    wrapper.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;';
+    target.appendChild(wrapper);
+    portalWrapper.value = wrapper;
+    return wrapper;
+  };
+
   const taskList = ref<Task[]>([]);
   const taskList = ref<Task[]>([]);
 
 
   // 已从接口拉取过的会话 id,用于识别流式期间后端新建的会话
   // 已从接口拉取过的会话 id,用于识别流式期间后端新建的会话
@@ -415,13 +432,6 @@
 
 
   const isFullscreen = ref(!!props.defaultFullscreen);
   const isFullscreen = ref(!!props.defaultFullscreen);
 
 
-  // 响应式视口宽度,用于计算 modal 最大宽度不超过屏幕
-  const viewportWidth = ref(window.innerWidth);
-  const onResize = () => {
-    viewportWidth.value = window.innerWidth;
-  };
-  window.addEventListener('resize', onResize);
-
   const currentTask = computed(() => taskList.value.find((t) => t.id === currentTaskId.value));
   const currentTask = computed(() => taskList.value.find((t) => t.id === currentTaskId.value));
 
 
   const currentRightPanels = computed<RightPanelState>(() => {
   const currentRightPanels = computed<RightPanelState>(() => {
@@ -439,10 +449,14 @@
   const isPanelLeaving = ref(false);
   const isPanelLeaving = ref(false);
   const modalShouldExpand = computed(() => hasRightPanel.value || isPanelLeaving.value);
   const modalShouldExpand = computed(() => hasRightPanel.value || isPanelLeaving.value);
 
 
-  // modal 宽度:全屏100vw / 有右侧面板时1600px(不超视口)/ 默认1400px
+  // modal 宽度:全屏时用容器实际宽度(容器自带 scale 统一缩放适配视口),挂 body 时用 100vw;
+  // 有右侧面板时 1600 / 默认 1400(设计稿像素)。高度由 CSS 的 height:100% 撑满容器
   const modalWidth = computed(() => {
   const modalWidth = computed(() => {
-    if (isFullscreen.value) return '100vw';
-    if (modalShouldExpand.value) return Math.min(1600, viewportWidth.value);
+    if (isFullscreen.value) {
+      const container = props.getContainer ? props.getContainer() : document.body;
+      return container === document.body ? '100vw' : `${container.clientWidth}px`;
+    }
+    if (modalShouldExpand.value) return 1600;
     return 1400;
     return 1400;
   });
   });
 
 
@@ -1125,13 +1139,7 @@
     const msgIndex = messages.value.findIndex((m) => m.attachedFile?.id === file.id);
     const msgIndex = messages.value.findIndex((m) => m.attachedFile?.id === file.id);
     if (msgIndex !== -1) {
     if (msgIndex !== -1) {
       nextTick(() => {
       nextTick(() => {
-        const messageElements = chatMessagesRef.value?.messagesRef?.querySelectorAll('.message-wrapper');
-        if (messageElements && messageElements[msgIndex]) {
-          (messageElements[msgIndex] as HTMLElement).scrollIntoView({
-            behavior: 'smooth',
-            block: 'center',
-          });
-        }
+        chatMessagesRef.value?.scrollToMessage(msgIndex, 'center');
       });
       });
     }
     }
   };
   };
@@ -1987,6 +1995,9 @@
   let dragStartY = 0;
   let dragStartY = 0;
   let modalStartLeft = 0;
   let modalStartLeft = 0;
   let modalStartTop = 0;
   let modalStartTop = 0;
+  // 容器 scale 缩放比(视觉尺寸/布局尺寸),用于把鼠标视口坐标换算回布局坐标
+  let dragScaleX = 1;
+  let dragScaleY = 1;
   let draggedEl: HTMLElement | null = null;
   let draggedEl: HTMLElement | null = null;
 
 
   const findModalWrap = (target: HTMLElement): HTMLElement | null =>
   const findModalWrap = (target: HTMLElement): HTMLElement | null =>
@@ -2023,17 +2034,20 @@
     draggedEl = findModalWrap(e.target as HTMLElement) || queryModalWrap();
     draggedEl = findModalWrap(e.target as HTMLElement) || queryModalWrap();
     if (!draggedEl) return;
     if (!draggedEl) return;
 
 
+    // 记录容器缩放比,并把鼠标/弹框的视口坐标换算回布局坐标,避免不同分辨率下拖拽位移失真
+    const rect = draggedEl.getBoundingClientRect();
+    dragScaleX = draggedEl.offsetWidth ? rect.width / draggedEl.offsetWidth : 1;
+    dragScaleY = draggedEl.offsetHeight ? rect.height / draggedEl.offsetHeight : 1;
+
     isDragging = true;
     isDragging = true;
     dragStartX = e.clientX;
     dragStartX = e.clientX;
     dragStartY = e.clientY;
     dragStartY = e.clientY;
-
-    const rect = draggedEl.getBoundingClientRect();
-    modalStartLeft = rect.left;
-    modalStartTop = rect.top;
+    modalStartLeft = rect.left / dragScaleX;
+    modalStartTop = rect.top / dragScaleY;
 
 
     draggedEl.style.position = 'fixed';
     draggedEl.style.position = 'fixed';
-    draggedEl.style.left = `${rect.left}px`;
-    draggedEl.style.top = `${rect.top}px`;
+    draggedEl.style.left = `${modalStartLeft}px`;
+    draggedEl.style.top = `${modalStartTop}px`;
     draggedEl.style.margin = '0';
     draggedEl.style.margin = '0';
     draggedEl.style.transform = 'none';
     draggedEl.style.transform = 'none';
 
 
@@ -2043,8 +2057,8 @@
 
 
   const onDragMove = (e: MouseEvent) => {
   const onDragMove = (e: MouseEvent) => {
     if (!isDragging || !draggedEl) return;
     if (!isDragging || !draggedEl) return;
-    draggedEl.style.left = `${modalStartLeft + (e.clientX - dragStartX)}px`;
-    draggedEl.style.top = `${modalStartTop + (e.clientY - dragStartY)}px`;
+    draggedEl.style.left = `${modalStartLeft + (e.clientX - dragStartX) / dragScaleX}px`;
+    draggedEl.style.top = `${modalStartTop + (e.clientY - dragStartY) / dragScaleY}px`;
   };
   };
 
 
   const onDragEnd = () => {
   const onDragEnd = () => {
@@ -2065,7 +2079,9 @@
   onBeforeUnmount(() => {
   onBeforeUnmount(() => {
     document.removeEventListener('mousemove', onDragMove);
     document.removeEventListener('mousemove', onDragMove);
     document.removeEventListener('mouseup', onDragEnd);
     document.removeEventListener('mouseup', onDragEnd);
-    window.removeEventListener('resize', onResize);
+    // 移除挂载空壳,避免残留
+    portalWrapper.value?.remove();
+    portalWrapper.value = null;
     // 组件卸载时主动断开所有任务的流式连接
     // 组件卸载时主动断开所有任务的流式连接
     taskAbortControllers.forEach((controller) => controller.abort());
     taskAbortControllers.forEach((controller) => controller.abort());
     // Abort in-flight FileReader if any
     // Abort in-flight FileReader if any
@@ -2297,8 +2313,9 @@
 
 
     &.fullscreen {
     &.fullscreen {
       top: 0 !important;
       top: 0 !important;
+      height: 100% !important;
       padding: 0 !important;
       padding: 0 !important;
-      max-width: 100vw !important;
+      max-width: none !important;
       margin: 0 !important;
       margin: 0 !important;
       background-color: #09172c;
       background-color: #09172c;
 
 
@@ -2308,11 +2325,15 @@
         margin: 0 !important;
         margin: 0 !important;
         top: 0 !important;
         top: 0 !important;
       }
       }
+      .zxm-modal-close,
+      .maximize-btn { 
+        top: 40px;
+      }
 
 
       .zxm-modal-content {
       .zxm-modal-content {
-        width: 100vw !important;
-        height: 100vh !important;
-        max-width: 100vw !important;
+        width: 100% !important;
+        height: 100% !important;
+        max-width: 100% !important;
         margin: 0;
         margin: 0;
         border-radius: 0;
         border-radius: 0;
         overflow: hidden;
         overflow: hidden;
@@ -2333,7 +2354,7 @@
         height: 90px;
         height: 90px;
       }
       }
       .zxm-modal-body {
       .zxm-modal-body {
-        height: calc(100vh - 120px);
+        height: calc(100% - 120px);
         overflow: hidden;
         overflow: hidden;
         padding: 0 24px 12px;
         padding: 0 24px 12px;
       }
       }

+ 24 - 7
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -769,7 +769,7 @@
     if (!n) return;
     if (!n) return;
     const el = document.getElementById(`citation-${msgIndex}-${n}`);
     const el = document.getElementById(`citation-${msgIndex}-${n}`);
     if (el) {
     if (el) {
-      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
+      scrollMessagesTo(el, 'center');
       el.classList.add('citation-highlight');
       el.classList.add('citation-highlight');
       setTimeout(() => el.classList.remove('citation-highlight'), 1500);
       setTimeout(() => el.classList.remove('citation-highlight'), 1500);
     }
     }
@@ -939,7 +939,10 @@
     if (!nav) return;
     if (!nav) return;
     // 横线数量变化(新增问答/首次悬浮)时重建坐标
     // 横线数量变化(新增问答/首次悬浮)时重建坐标
     if (lineCenters.value.length !== qaPairs.value.length) updateLineCenters();
     if (lineCenters.value.length !== qaPairs.value.length) updateLineCenters();
-    navMouseY.value = e.clientY - nav.getBoundingClientRect().top + nav.scrollTop;
+    // e.clientY / getBoundingClientRect 是容器 scale 缩放后的视口坐标,需换算回布局坐标
+    // (÷ 容器竖向缩放)后再与 lineCenters / scrollTop 比较,否则不同分辨率下定位错位
+    const scale = nav.offsetHeight ? nav.getBoundingClientRect().height / nav.offsetHeight : 1;
+    navMouseY.value = (e.clientY - nav.getBoundingClientRect().top) / scale + nav.scrollTop;
     const nearest = nearestLineIndex.value;
     const nearest = nearestLineIndex.value;
     if (nearest >= 0 && nearest !== lastTooltipIndex) {
     if (nearest >= 0 && nearest !== lastTooltipIndex) {
       lastTooltipIndex = nearest;
       lastTooltipIndex = nearest;
@@ -958,17 +961,31 @@
     if (lastTooltipIndex >= 0) applyTooltip(lastTooltipIndex);
     if (lastTooltipIndex >= 0) applyTooltip(lastTooltipIndex);
   };
   };
 
 
+  // 仅滚动消息列表容器:scrollIntoView 会联动滚动 #adaptive-container 等祖先,导致整页上移、底部露出空白
+  const scrollMessagesTo = (el: HTMLElement, block: 'start' | 'center' = 'start') => {
+    const container = messagesRef.value;
+    if (!container) return;
+    const top = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
+    const offset = block === 'center' ? (container.clientHeight - el.offsetHeight) / 2 : 0;
+    container.scrollTo({ top: Math.max(0, top - offset), behavior: 'smooth' });
+  };
+
+  const scrollToMessage = (index: number, block: 'start' | 'center' = 'start') => {
+    const wrappers = messagesRef.value?.querySelectorAll('.message-wrapper');
+    const el = wrappers?.[index] as HTMLElement | undefined;
+    if (el) scrollMessagesTo(el, block);
+  };
+
   const scrollToPair = (pair: QaPair) => {
   const scrollToPair = (pair: QaPair) => {
     activeMessage.value = props.messages[pair.startIndex];
     activeMessage.value = props.messages[pair.startIndex];
+    scrollToMessage(pair.startIndex, 'start');
     const wrappers = messagesRef.value?.querySelectorAll('.message-wrapper');
     const wrappers = messagesRef.value?.querySelectorAll('.message-wrapper');
     const el = wrappers?.[pair.startIndex] as HTMLElement | undefined;
     const el = wrappers?.[pair.startIndex] as HTMLElement | undefined;
-    if (!el) return;
-    el.scrollIntoView({ behavior: 'smooth', block: 'start' });
-    el.classList.add('nav-located-highlight');
-    setTimeout(() => el.classList.remove('nav-located-highlight'), 1500);
+    el?.classList.add('nav-located-highlight');
+    setTimeout(() => el?.classList.remove('nav-located-highlight'), 1500);
   };
   };
 
 
-  defineExpose({ messagesRef, isAtBottom, forceScrollToBottom, initAskUserAnswers });
+  defineExpose({ messagesRef, isAtBottom, forceScrollToBottom, scrollToMessage, initAskUserAnswers });
 </script>
 </script>
 
 
 <style scoped lang="less">
 <style scoped lang="less">