diff --git a/src/layout/components/Aichat/ChatPopup.vue b/src/layout/components/Aichat/ChatPopup.vue index bc41571..e1a9d4a 100644 --- a/src/layout/components/Aichat/ChatPopup.vue +++ b/src/layout/components/Aichat/ChatPopup.vue @@ -168,6 +168,13 @@ export default { // 组件销毁标志 isDestroyed: false, + // 性能优化相关 + scrollThrottleTimer: null, // 滚动节流定时器 + contentUpdateTimer: null, // 内容更新节流定时器 + isUserAtBottom: true, // 用户是否在底部 + lastContentLength: 0, // 上次内容长度 + scrollPending: false, // 滚动待处理标志 + // 性能优化和错误边界 requestQueue: [], maxRetries: 2, @@ -283,6 +290,14 @@ export default { clearTimeout(this.loadDebounceTimer) this.loadDebounceTimer = null } + if (this.scrollThrottleTimer) { + clearTimeout(this.scrollThrottleTimer) + this.scrollThrottleTimer = null + } + if (this.contentUpdateTimer) { + clearTimeout(this.contentUpdateTimer) + this.contentUpdateTimer = null + } // 取消正在进行的请求 if (this.currentCancel) { @@ -809,8 +824,8 @@ export default { if (data.event === 'message' && data.answer) { const currentContent = aiMsg.content const newContent = (currentContent === '正在思考...' ? '' : currentContent) + data.answer - aiMsg.content = newContent - this.scrollToBottom(true) // 流式响应时使用平滑滚动 + // 使用节流更新方法,减少频繁的DOM更新和滚动 + this.throttledContentUpdate(aiMsg, newContent) } // 处理结束消息 @@ -825,12 +840,19 @@ export default { userMsg.messageId = data.message_id aiMsg.messageId = 'ai-' + data.message_id } - if (data.retriever_resources) { - aiMsg.retrieverResources = data.retriever_resources - aiMsg.groupedReferences = this.getGroupedReferences(data.retriever_resources) // 预计算分组引用 + if (data.metadata && data.metadata.retriever_resources) { + // 使用Vue的响应式方法确保引用信息正确更新 + this.$set(aiMsg, 'retrieverResources', data.metadata.retriever_resources) + this.$set(aiMsg, 'groupedReferences', this.getGroupedReferences(data.metadata.retriever_resources)) } - // 标记流输出完成 + // 标记流输出完成并触发最终滚动 aiMsg.streamCompleted = true + // 重置内容长度计数器 + this.lastContentLength = 0 + // 流式响应结束后强制滚动到底部 + this.$nextTick(() => { + this.scrollToBottom(true, true) // 使用平滑滚动并强制执行 + }) } // 处理错误事件 @@ -933,45 +955,48 @@ export default { }, /** - * 滚动到底部 + * 滚动到底部(优化版本) */ - scrollToBottom(smooth = false) { - this.$nextTick(() => { - if (this.$refs.messageList) { - const targetTop = this.$refs.messageList.scrollHeight - const messageList = this.$refs.messageList + scrollToBottom(smooth = false, force = false) { + // 如果用户不在底部且不是强制滚动,则跳过 + if (!force && !this.isUserAtBottom) { + return + } - if (smooth && messageList.scrollTo && !this.isLoadingHistory) { - // 只在非加载状态时使用平滑滚动 - messageList.scrollTo({ - top: messageList.scrollHeight, - behavior: 'smooth' - }) - } else { - // 强制立即滚动到底部 - const forceScrollToBottom = () => { - messageList.scrollTop = messageList.scrollHeight + // 节流处理,避免频繁滚动 + if (this.scrollThrottleTimer) { + clearTimeout(this.scrollThrottleTimer) + } + + this.scrollThrottleTimer = setTimeout(() => { + this.$nextTick(() => { + if (this.$refs.messageList && !this.isDestroyed) { + const messageList = this.$refs.messageList + const scrollHeight = messageList.scrollHeight + + if (smooth && messageList.scrollTo && !this.isLoadingHistory) { + // 只在非加载状态时使用平滑滚动 + messageList.scrollTo({ + top: scrollHeight, + behavior: 'smooth' + }) + } else { + // 立即滚动到底部 + messageList.scrollTop = scrollHeight } - // 立即执行一次 - forceScrollToBottom() - - // 再次确保滚动到底部 - this.$nextTick(() => { - forceScrollToBottom() - // 第三次确保 - setTimeout(() => { - forceScrollToBottom() - }, 10) - }) + // 更新用户位置状态 + this.isUserAtBottom = true + + // 延迟更新lastScrollTop + setTimeout(() => { + if (!this.isDestroyed) { + this.lastScrollTop = scrollHeight + } + }, smooth ? 300 : 50) } - - // 延迟更新lastScrollTop,避免干扰滚动检测 - setTimeout(() => { - this.lastScrollTop = this.$refs.messageList.scrollHeight - }, smooth ? 300 : 50) // 非平滑滚动时更快更新 - } - }) + }) + }, smooth ? 0 : 16) // 非平滑滚动时使用16ms节流(约60fps) }, /** @@ -979,6 +1004,11 @@ export default { */ onScroll(e) { const scrollTop = e.target.scrollTop + const scrollHeight = e.target.scrollHeight + const clientHeight = e.target.clientHeight + + // 检测用户是否在底部(允许10px的误差) + this.isUserAtBottom = (scrollTop + clientHeight >= scrollHeight - 10) // 检查是否需要加载历史记录 // 当滚动到距离顶部阈值范围内且向上滚动时触发加载 @@ -1036,6 +1066,37 @@ export default { return sanitizedHtml }, + /** + * 节流更新内容 + */ + throttledContentUpdate(aiMsg, newContent) { + // 检查内容是否真的有变化 + if (aiMsg.content === newContent) { + return + } + + // 立即更新内容(保证响应性) + aiMsg.content = newContent + + // 只有当内容长度显著增加时才滚动 + const contentLengthDiff = newContent.length - this.lastContentLength + if (contentLengthDiff > 20) { // 内容增加超过20个字符才滚动 + this.lastContentLength = newContent.length + + // 清除之前的定时器 + if (this.contentUpdateTimer) { + clearTimeout(this.contentUpdateTimer) + } + + // 节流滚动更新 + this.contentUpdateTimer = setTimeout(() => { + if (this.isUserAtBottom && !this.isDestroyed) { + this.scrollToBottom(false) + } + }, 100) // 100ms节流,减少滚动频率 + } + }, + /** * 处理点赞 */