From 0450e9d979239406671baf34a4c0b86d84662ac8 Mon Sep 17 00:00:00 2001 From: digua Date: Sun, 21 Dec 2025 22:46:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96AI=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/analysis/ai/ChatExplorer.vue | 107 ++++++++++++++++++-- src/components/analysis/ai/ChatMessage.vue | 13 +++ 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/components/analysis/ai/ChatExplorer.vue b/src/components/analysis/ai/ChatExplorer.vue index 3bf611b3..a5aed251 100644 --- a/src/components/analysis/ai/ChatExplorer.vue +++ b/src/components/analysis/ai/ChatExplorer.vue @@ -78,6 +78,11 @@ const isCheckingConfig = ref(true) const messagesContainer = ref(null) const conversationListRef = ref | null>(null) +// 智能滚动状态 +const isStickToBottom = ref(true) // 是否粘在底部(自动滚动) +const showScrollToBottom = ref(false) // 是否显示"返回底部"按钮 +const RESTICK_THRESHOLD = 30 // 距离底部此距离内时重新粘住 + // 截屏功能 const conversationContentRef = ref(null) @@ -161,21 +166,55 @@ ${configHint}` // 发送消息 async function handleSend(content: string) { await sendMessage(content) - // 滚动到底部 - scrollToBottom() + // 强制滚动到底部(用户发送消息后应该看到响应) + scrollToBottom(true) // 刷新对话列表 conversationListRef.value?.refresh() } -// 滚动到底部 -function scrollToBottom() { +// 滚动到底部(强制滚动,用于发送消息等场景) +function scrollToBottom(force = false) { setTimeout(() => { if (messagesContainer.value) { - messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight + // 如果强制滚动,或者处于粘性模式,才执行滚动 + if (force || isStickToBottom.value) { + messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight + isStickToBottom.value = true + showScrollToBottom.value = false + } } }, 100) } +// 处理用户滚轮/触控板事件(可靠地检测用户主动滚动) +function handleWheel(event: WheelEvent) { + // deltaY < 0 表示向上滚动 + if (event.deltaY < 0 && isAIThinking.value) { + // 用户在 AI 生成时主动向上滚动,解除粘性 + isStickToBottom.value = false + showScrollToBottom.value = true + } +} + +// 检测滚动位置(仅用于检测是否滚动到底部以重新粘住) +function checkScrollPosition() { + if (!messagesContainer.value) return + + const { scrollTop, scrollHeight, clientHeight } = messagesContainer.value + const distanceFromBottom = scrollHeight - scrollTop - clientHeight + + // 如果用户手动滚动到接近底部,重新启用粘性 + if (distanceFromBottom < RESTICK_THRESHOLD) { + isStickToBottom.value = true + showScrollToBottom.value = false + } +} + +// 点击"返回底部"按钮 +function handleScrollToBottom() { + scrollToBottom(true) +} + // 切换数据源面板 function toggleSourcePanel() { isSourcePanelCollapsed.value = !isSourcePanelCollapsed.value @@ -189,7 +228,7 @@ async function handleLoadMore() { // 选择对话 async function handleSelectConversation(convId: string) { await loadConversation(convId) - scrollToBottom() + scrollToBottom(true) // 切换对话时强制滚动到底部 } // 创建新对话 @@ -212,11 +251,21 @@ onMounted(async () => { // 初始化欢迎消息 startNewConversation(generateWelcomeMessage()) + + // 添加事件监听 + if (messagesContainer.value) { + messagesContainer.value.addEventListener('scroll', checkScrollPosition) + messagesContainer.value.addEventListener('wheel', handleWheel, { passive: true }) + } }) -// 组件卸载时停止生成 +// 组件卸载时清理 onBeforeUnmount(() => { stopGeneration() + if (messagesContainer.value) { + messagesContainer.value.removeEventListener('scroll', checkScrollPosition) + messagesContainer.value.removeEventListener('wheel', handleWheel) + } }) // 处理停止按钮 @@ -240,6 +289,14 @@ watch( } ) +// 监听 AI 响应 contentBlocks 更新(工具调用状态变化) +watch( + () => messages.value[messages.value.length - 1]?.contentBlocks?.length, + () => { + scrollToBottom() + } +) + // 监听全局 AI 配置变化(从设置弹窗保存时触发) watch( () => promptStore.aiConfigVersion, @@ -264,7 +321,7 @@ watch(
-
+
@@ -308,8 +365,15 @@ watch(
- -
+ +
@@ -386,6 +450,18 @@ watch(
+ + + + +
@@ -499,4 +575,15 @@ watch( transform: translateY(10px); opacity: 0; } + +/* Transition styles for fade-up (scroll to bottom button) */ +.fade-up-enter-active, +.fade-up-leave-active { + transition: opacity 0.2s ease-out; +} + +.fade-up-enter-from, +.fade-up-leave-to { + opacity: 0; +} diff --git a/src/components/analysis/ai/ChatMessage.vue b/src/components/analysis/ai/ChatMessage.vue index eb5b7cbf..9af610e9 100644 --- a/src/components/analysis/ai/ChatMessage.vue +++ b/src/components/analysis/ai/ChatMessage.vue @@ -253,6 +253,19 @@ function formatToolParams(tool: ToolBlockContent): string {
+ + +
+ + + + + + 正在生成回复... +