From 059b93ddbbeeab97ecc6735567ef281ba0eaa9d9 Mon Sep 17 00:00:00 2001 From: WangJing Date: Wed, 30 Jul 2025 15:30:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B6=88=E6=81=AF=E4=BD=93=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/chatweb/components/ChatBox.vue | 101 +++++++++++++----- 1 file changed, 76 insertions(+), 25 deletions(-) diff --git a/chat-client/src/views/chatweb/components/ChatBox.vue b/chat-client/src/views/chatweb/components/ChatBox.vue index dc8dfcc..69b4df4 100644 --- a/chat-client/src/views/chatweb/components/ChatBox.vue +++ b/chat-client/src/views/chatweb/components/ChatBox.vue @@ -281,6 +281,8 @@ +
+ @@ -382,11 +384,12 @@ import { ref, reactive, nextTick, watch, onMounted } from 'vue' import { ElMessage } from 'element-plus' import VueOfficePdf from '@vue-office/pdf' import VabChart from '@/plugins/VabChart/index.vue' - +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/solid' import { sendChatMessage, type ChatMessageResponse, type ChatMessageSendRequest, stopMessagesStream, getFilePathList, type TraceFile, type TraceData } from '@/api/chat' import { useI18n } from 'vue-i18n' import { throttle } from 'lodash' +const expanded = ref(false) const { t, locale } = useI18n() // Props 定义 interface Props { @@ -441,11 +444,12 @@ interface Message { isOpenRemark?: boolean // 是否为开场白消息 } - +// 消息体底部锚点 +const bottomAnchor = ref(null) const messages = ref([]) const inputMessage = ref('') -const messagesContainer = ref() +const messagesContainer = ref(null) const isLoading = ref(false) const currentTaskId = ref('') const showStopButton = ref(false) @@ -563,7 +567,7 @@ const createNewChat = async () => { // 滚动到底部显示新消息 await nextTick() - await scrollToBottom() + throttledScrollToBottom() } // 切换会话 @@ -632,6 +636,10 @@ watch(messages, (newMessages) => { } }, { deep: true }) + +// 用户是否正在拖动滚动条 +let isUserScrolling = false + // 初始化时加载会话历史 onMounted(async () => { loadChatHistory() @@ -639,6 +647,15 @@ onMounted(async () => { if (chatHistory.value.length === 0) { await createNewChat() } + + //监听用户是否拖动滚动条 + messagesContainer.value?.addEventListener('scroll', () => { + const el = messagesContainer.value + if (!el) return + const threshold = 100 + isUserScrolling = el.scrollHeight - el.scrollTop - el.clientHeight > threshold + }) + }) // 解析ECharts代码块 @@ -683,10 +700,15 @@ const formatAnswer = async (text: string) => { // 为表格添加容器包装和强制样式属性 const wrappedHtml = html.replace( //g, - '
' + `
+ + Expand / Collapse table + +
` ).replace( - /<\/table>/g, - '
' + /<\/table>/g, + '' + ).replace( //g, '' @@ -700,9 +722,9 @@ const formatAnswer = async (text: string) => { 'strong', 'em', 'code', 'pre', 'a', 'br', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'span', 'blockquote', 'hr', 'table', 'thead', 'tbody', - 'tr', 'th', 'td', 'div' + 'tr', 'th', 'td', 'div', 'details', 'summary' ], - ALLOWED_ATTR: ['href', 'target', 'class', 'style', 'data-chart-id', 'border', 'cellspacing', 'cellpadding'] + ALLOWED_ATTR: ['href', 'target', 'class', 'style', 'data-chart-id', 'border', 'cellspacing', 'cellpadding', 'open'] }) as string console.log('Markdown输入:', processedText) @@ -801,9 +823,6 @@ const sendMessage = async () => { currentTaskId.value = data.taskId } - // messages.value = [...messages.value] - // botMessage.text += data.answer - // await scrollToBottom() throttledScrollToBottom() } } catch (e) { @@ -824,7 +843,6 @@ const sendMessage = async () => { const formatted = await formatAnswer(parseThink(botMessage.text).answer) botMessage.formattedText = formatted.html botMessage.echarts = formatted.echarts - // messages.value = [...messages.value] if (data.conversationId) { newConversationId = data.conversationId @@ -852,7 +870,6 @@ const sendMessage = async () => { botMessage.sources = data botMessage.conversationId = newConversationId botMessage.messageId = newMessageId - // messages.value = [...messages.value] } } catch (error) { console.log('获取文件来源失败:', error) @@ -885,20 +902,35 @@ const sendMessage = async () => { if (messages.value[messages.value.length - 1]?.isLoading) { messages.value[messages.value.length - 1].isLoading = false } - await scrollToBottom() + scrollToBottomForce() } } -const scrollToBottom = async () => { +const scrollToBottomForce = async () => { await nextTick() if (messagesContainer.value) { messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight } } +//将锚点滚动到最底部 +const scrollToBottom = async () => { + await nextTick() + setTimeout(() => { + requestAnimationFrame(() => { + if (messagesContainer.value && !isUserScrolling) { + bottomAnchor.value.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }) + } + }) + },60) +} + const throttledScrollToBottom = throttle(() => { - scrollToBottom() -}, 150) + scrollToBottom() +}, 300,{ leading: true, trailing: true }) // 解析方法 const parseThink = (text: string) => { @@ -1227,7 +1259,7 @@ const regenerateResponse = async (userQuery: string) => { formattedText: '' }) messages.value.push(botMessage) - await scrollToBottom() + throttledScrollToBottom() // 获取当前会话的会话ID const currentSession = chatHistory.value[currentChatIndex.value] @@ -1287,8 +1319,7 @@ const regenerateResponse = async (userQuery: string) => { currentTaskId.value = data.taskId } - // messages.value = [...messages.value] - await scrollToBottom() + throttledScrollToBottom() } } catch (e) { console.error('解析错误:', e) @@ -1308,7 +1339,6 @@ const regenerateResponse = async (userQuery: string) => { const formatted = await formatAnswer(parseThink(botMessage.text).answer) botMessage.formattedText = formatted.html botMessage.echarts = formatted.echarts - // messages.value = [...messages.value] if (data.conversationId) { newConversationId = data.conversationId @@ -1336,7 +1366,6 @@ const regenerateResponse = async (userQuery: string) => { botMessage.sources = data botMessage.conversationId = newConversationId botMessage.messageId = newMessageId - // messages.value = [...messages.value] } } catch (error) { console.log('获取文件来源失败:', error) @@ -1369,7 +1398,7 @@ const regenerateResponse = async (userQuery: string) => { if (messages.value[messages.value.length - 1]?.isLoading) { messages.value[messages.value.length - 1].isLoading = false } - await scrollToBottom() + scrollToBottomForce() } } @@ -1399,7 +1428,7 @@ const handleRecommendQuestionClick = (question: string) => {