@@ -168,11 +175,23 @@ export default {
performanceMonitor: null,
memoryCheckInterval: null,
lastMemoryUsage: 0,
-
+
// 请求监控
activeRequests: new Map(), // 存储活跃请求的信息
requestMonitorInterval: null,
- maxRequestDuration: 60000 // 最大请求持续时间60秒
+ maxRequestDuration: 60000, // 最大请求持续时间60秒
+
+ // Markdown渲染缓存
+ markdownCache: new Map(), // 缓存已渲染的Markdown内容
+ maxCacheSize: 100, // 最大缓存条目数
+
+ // 引用资源分组缓存
+ referencesCache: new Map(), // 缓存引用资源分组结果
+ maxReferencesCacheSize: 50, // 最大引用缓存条目数
+
+ // 消息显示优化
+ maxVisibleMessages: 50, // 最大可见消息数量
+ messageRenderBatch: 20, // 每批渲染的消息数量
}
},
computed: {
@@ -189,6 +208,14 @@ export default {
// 获取用户名
userName() {
return this.name || localStorage.getItem('userName') || '用户'
+ },
+ // 获取可见消息列表(性能优化)
+ visibleMessages() {
+ if (this.messages.length <= this.maxVisibleMessages) {
+ return this.messages
+ }
+ // 只显示最新的消息,保持对话连续性
+ return this.messages.slice(-this.maxVisibleMessages)
}
},
watch: {
@@ -200,9 +227,9 @@ export default {
if (!this.isDestroyed) {
setTimeout(() => {
if (!this.isDestroyed) {
- this.scrollToBottom(false)
- }
- }, 100)
+ this.scrollToBottom(false)
+ }
+ }, 100)
}
})
}
@@ -222,7 +249,7 @@ export default {
// 启动性能监控
this.startPerformanceMonitoring()
-
+
// 启动请求监控
this.startRequestMonitoring()
@@ -240,38 +267,44 @@ export default {
beforeDestroy() {
// 设置销毁标志
this.isDestroyed = true
-
+
// 移除全局错误处理器
window.removeEventListener('unhandledrejection', this.handleUnhandledRejection)
window.removeEventListener('error', this.handleGlobalError)
-
+
// 停止性能监控
this.stopPerformanceMonitoring()
-
+
// 停止请求监控
this.stopRequestMonitoring()
-
+
// 清理定时器
if (this.loadDebounceTimer) {
clearTimeout(this.loadDebounceTimer)
this.loadDebounceTimer = null
}
-
+
// 取消正在进行的请求
if (this.currentCancel) {
this.currentCancel()
this.currentCancel = null
}
-
+
// 清理请求队列
this.requestQueue = []
-
+
// 清理消息数据
this.messages = []
-
+
// 清理引用展示状态
this.showSingleReference = {}
-
+
+ // 清理markdown缓存
+ this.markdownCache.clear()
+
+ // 清理引用资源分组缓存
+ this.referencesCache.clear()
+
// 重置所有状态
this.conversation_id = ''
this.earliestMessageId = null
@@ -280,7 +313,7 @@ export default {
this.isLoadingHistory = false
this.hasMoreHistory = false
this.initFailed = false
-
+
// 强制垃圾回收(如果浏览器支持)
if (window.gc) {
try {
@@ -299,25 +332,25 @@ export default {
if (this.isLoadingHistory || this.isDestroyed) {
return
}
-
+
try {
this.isLoadingHistory = true
this.retryCount = 0
-
+
// 添加到请求队列
const requestId = Date.now()
this.requestQueue.push(requestId)
-
+
// 如果队列中有太多请求,清理旧请求
if (this.requestQueue.length > 3) {
this.requestQueue = this.requestQueue.slice(-3)
}
-
+
// 添加超时控制,避免卡死
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 8000) // 减少到8秒
})
-
+
// 创建取消Promise
const cancelPromise = new Promise((_, reject) => {
const checkCancel = () => {
@@ -338,22 +371,24 @@ export default {
// 使用Promise.race来实现超时和取消控制
const res = await Promise.race([historyPromise, timeoutPromise, cancelPromise])
-
+
// 从队列中移除当前请求
this.requestQueue = this.requestQueue.filter(id => id !== requestId)
-
+ // console.log('历史记录响应:', res);
if (this.isDestroyed) {
return
}
if (res.code === 200 && res.data && Array.isArray(res.data.data)) {
const newMessages = []
+ // console.log('历史记录:', res.data.data)
- // 处理历史消息
+ // 批量处理历史消息,减少DOM操作
+ const messagesBatch = []
res.data.data.forEach(msg => {
// 用户消息
if (msg.query) {
- newMessages.push({
+ messagesBatch.push({
sender: 'user',
avatar: require('@/assets/ai/yonghu.png'),
content: msg.query,
@@ -363,9 +398,10 @@ export default {
})
}
- // AI消息
+ // AI消息 - 延迟计算groupedReferences
if (msg.answer) {
- newMessages.push({
+ const retrieverResources = msg.retriever_resources || []
+ const aiMessage = {
sender: 'ai',
avatar: require('@/assets/ai/AI.png'),
content: msg.answer,
@@ -373,18 +409,32 @@ export default {
conversationId: msg.conversation_id,
created_at: msg.created_at,
feedback: msg.feedback || null,
- retrieverResources: msg.retriever_resources || [],
+ retrieverResources: retrieverResources,
streamCompleted: true // 历史消息的流输出已完成
- })
+ }
+
+ // 只有在有引用资源时才预计算分组
+ if (retrieverResources && retrieverResources.length > 0) {
+ // 使用异步方式计算,避免阻塞主线程
+ this.$nextTick(() => {
+ aiMessage.groupedReferences = this.getGroupedReferences(retrieverResources)
+ })
+ }
+
+ messagesBatch.push(aiMessage)
}
})
// 按时间排序(从旧到新)
- newMessages.sort((a, b) => {
+ messagesBatch.sort((a, b) => {
return a.created_at - b.created_at
})
- this.messages = newMessages
+ newMessages.push(...messagesBatch)
+ // console.log('处理后的历史记录:', newMessages)
+
+ // 分批渲染消息,避免一次性渲染过多消息导致卡死
+ this.renderMessagesInBatches(newMessages)
if (newMessages.length > 0) {
this.conversation_id = newMessages[0].conversationId
@@ -398,28 +448,30 @@ export default {
this.earliestMessageId = newMessages[0].messageId.replace('ai-', '')
}
}
-
+ // console.log('历史记录:', newMessages)
this.hasMoreHistory = res.data.has_more || false
} else {
+ // console.log('没有历史记录')
// 没有历史记录,显示欢迎消息
- this.messages = [{
+ const welcomeMessages = [{
sender: 'ai',
avatar: require('@/assets/ai/AI.png'),
content: '你好!我是智水AI辅导员,有什么可以帮助你的吗?',
messageId: 'welcome-' + Date.now()
}]
+ this.renderMessagesInBatches(welcomeMessages)
this.hasMoreHistory = false
}
this.initFailed = false
} catch (error) {
console.error('初始化聊天失败:', error)
-
+
// 如果是取消错误,直接返回
if (error.message === '请求已取消' || this.isDestroyed) {
return
}
-
+
// 根据错误类型显示不同的提示信息
let errorMessage = '加载历史记录失败'
if (error.message === '请求超时') {
@@ -431,17 +483,18 @@ export default {
} else if (error.response && error.response.status >= 500) {
errorMessage = '服务器暂时不可用,请稍后重试'
}
-
+
this.showToast(errorMessage)
-
+
// 显示欢迎消息作为降级方案
- this.messages = [{
+ const fallbackMessages = [{
sender: 'ai',
avatar: require('@/assets/ai/AI.png'),
content: '你好!我是智水AI辅导员,有什么可以帮助你的吗?\n\n如果遇到网络问题,请稍后重试或联系管理员。',
messageId: 'welcome-' + Date.now()
}]
-
+ this.renderMessagesInBatches(fallbackMessages)
+
// 重置相关状态
this.hasMoreHistory = false
this.conversation_id = ''
@@ -449,10 +502,10 @@ export default {
this.initFailed = true // 标记初始化失败
} finally {
this.isLoadingHistory = false
-
+
// 清理请求队列
this.requestQueue = []
-
+
// 滚动到底部
if (!this.isDestroyed) {
this.$nextTick(() => {
@@ -516,6 +569,7 @@ export default {
// AI消息
if (msg.answer) {
+ const retrieverResources = msg.retriever_resources || []
newMessages.push({
sender: 'ai',
avatar: require('@/assets/ai/AI.png'),
@@ -524,7 +578,8 @@ export default {
conversationId: msg.conversation_id,
created_at: msg.created_at,
feedback: msg.feedback || null,
- retrieverResources: msg.retriever_resources || []
+ retrieverResources: retrieverResources,
+ groupedReferences: this.getGroupedReferences(retrieverResources) // 预计算分组引用
})
}
})
@@ -578,7 +633,7 @@ export default {
}
} catch (error) {
console.error('加载历史记录失败:', error)
-
+
// 根据错误类型显示不同的提示信息
let errorMessage = '加载历史记录失败'
if (error.message === '加载历史记录超时') {
@@ -588,9 +643,9 @@ export default {
} else if (error.response && error.response.status >= 500) {
errorMessage = '服务器繁忙,请稍后重试'
}
-
+
this.showToast(errorMessage)
-
+
// 如果是超时或网络错误,不改变hasMoreHistory状态,允许用户重试
if (!error.message || (!error.message.includes('超时') && !error.message.includes('Network'))) {
this.hasMoreHistory = false
@@ -608,20 +663,20 @@ export default {
if (this.isLoadingHistory || this.isDestroyed) {
return
}
-
+
// 检查重试次数
if (this.retryCount >= this.maxRetries) {
this.showToast('重试次数过多,请刷新页面')
return
}
-
+
this.initFailed = false
this.retryCount++
-
+
// 清理之前的状态
this.messages = []
this.requestQueue = []
-
+
try {
await this.initChat()
} catch (error) {
@@ -637,7 +692,7 @@ export default {
if (!this.inputMessage.trim() || this.sending || this.isDestroyed) {
return
}
-
+
// 防止频繁发送
if (this.requestQueue.length > 2) {
this.showToast('请等待当前消息处理完成')
@@ -647,7 +702,7 @@ export default {
const userMessage = this.inputMessage.trim()
this.inputMessage = ''
this.sending = true
-
+
// 添加到请求队列
const requestId = Date.now()
this.requestQueue.push(requestId)
@@ -686,7 +741,7 @@ export default {
})
this.currentCancel = cancel
-
+
// 注册请求到监控系统
this.activeRequests.set(requestId, {
startTime: Date.now(),
@@ -694,7 +749,7 @@ export default {
cancel: cancel,
userMessage: userMessage
})
-
+
// 添加额外的超时保护
const streamTimeout = setTimeout(() => {
if (cancel) {
@@ -704,10 +759,10 @@ export default {
aiMsg.streamCompleted = true
this.sending = false
}, 45000) // 45秒超时保护
-
+
const response = await stream
clearTimeout(streamTimeout) // 清除超时保护
-
+
const { reader, decoder } = response
let buffer = ''
let lastUpdateTime = Date.now()
@@ -725,17 +780,17 @@ export default {
aiMsg.content = aiMsg.content === '正在思考...' ? '服务器响应中断,请重新发送' : aiMsg.content + '\n\n[响应中断]'
aiMsg.streamCompleted = true
}, 15000) // 15秒无数据则认为中断
-
+
const { done, value } = await reader.read()
-
+
// 清除无数据超时
if (noDataTimeout) {
clearTimeout(noDataTimeout)
noDataTimeout = null
}
-
+
if (done) break
-
+
// 更新最后接收数据时间
lastUpdateTime = Date.now()
@@ -772,11 +827,12 @@ export default {
}
if (data.retriever_resources) {
aiMsg.retrieverResources = data.retriever_resources
+ aiMsg.groupedReferences = this.getGroupedReferences(data.retriever_resources) // 预计算分组引用
}
// 标记流输出完成
aiMsg.streamCompleted = true
}
-
+
// 处理错误事件
if (data.event === 'error') {
aiMsg.content = data.message || '服务器处理出错,请重新发送'
@@ -790,11 +846,11 @@ export default {
}
} catch (error) {
console.error('发送消息失败:', error)
-
+
// 清理所有定时器
if (streamTimeout) clearTimeout(streamTimeout)
if (noDataTimeout) clearTimeout(noDataTimeout)
-
+
// 根据错误类型显示不同的错误信息
let errorMessage = '发送消息失败'
if (error.message.includes('超时')) {
@@ -812,27 +868,70 @@ export default {
} else {
aiMsg.content = '抱歉,发送消息时出现错误,请稍后重试'
}
-
+
aiMsg.streamCompleted = true // 即使出错也标记为完成,显示操作区域
this.showToast(errorMessage, 'error')
} finally {
this.sending = false
this.currentCancel = null
-
+
// 清理所有定时器
if (streamTimeout) clearTimeout(streamTimeout)
if (noDataTimeout) clearTimeout(noDataTimeout)
-
+
// 从请求队列中移除当前请求
this.requestQueue = this.requestQueue.filter(id => id !== requestId)
-
+
// 从监控系统中移除请求
this.activeRequests.delete(requestId)
-
+
this.scrollToBottom(true) // 发送完成后使用平滑滚动
}
},
+ /**
+ * 分批渲染消息,避免一次性渲染过多消息导致卡死
+ */
+ renderMessagesInBatches(newMessages) {
+ if (!newMessages || newMessages.length === 0) {
+ this.messages = []
+ return
+ }
+
+ // 如果消息数量较少,直接渲染
+ if (newMessages.length <= this.messageRenderBatch) {
+ this.messages = newMessages
+ return
+ }
+
+ // 分批渲染
+ this.messages = []
+ let currentIndex = 0
+
+ const renderNextBatch = () => {
+ if (this.isDestroyed || currentIndex >= newMessages.length) {
+ return
+ }
+
+ const endIndex = Math.min(currentIndex + this.messageRenderBatch, newMessages.length)
+ const batch = newMessages.slice(currentIndex, endIndex)
+
+ // 添加当前批次的消息
+ this.messages.push(...batch)
+ currentIndex = endIndex
+
+ // 如果还有更多消息,继续渲染下一批
+ if (currentIndex < newMessages.length) {
+ this.$nextTick(() => {
+ setTimeout(renderNextBatch, 10) // 10ms延迟,让浏览器有时间处理当前批次
+ })
+ }
+ }
+
+ // 开始渲染第一批
+ renderNextBatch()
+ },
+
/**
* 滚动到底部
*/
@@ -911,12 +1010,30 @@ export default {
},
/**
- * 渲染Markdown内容
+ * 渲染Markdown内容(带缓存优化)
*/
renderMarkdown(text) {
if (!text) return ''
+
+ // 检查缓存
+ if (this.markdownCache.has(text)) {
+ return this.markdownCache.get(text)
+ }
+
+ // 渲染markdown
const html = this.md.render(text)
- return DOMPurify.sanitize(html)
+ const sanitizedHtml = DOMPurify.sanitize(html)
+
+ // 缓存管理:如果缓存超过最大大小,删除最旧的条目
+ if (this.markdownCache.size >= this.maxCacheSize) {
+ const firstKey = this.markdownCache.keys().next().value
+ this.markdownCache.delete(firstKey)
+ }
+
+ // 添加到缓存
+ this.markdownCache.set(text, sanitizedHtml)
+
+ return sanitizedHtml
},
/**
@@ -1000,13 +1117,25 @@ export default {
},
/**
- * 获取分组的引用来源
+ * 获取分组的引用来源(带缓存优化)
*/
getGroupedReferences(resources) {
if (!resources || !Array.isArray(resources)) {
return []
}
+ // 生成缓存键
+ const cacheKey = JSON.stringify(resources.map(r => ({
+ document_name: r.document_name,
+ id: r.id || r.chunk_id
+ })))
+
+ // 检查缓存
+ if (this.referencesCache.has(cacheKey)) {
+ return this.referencesCache.get(cacheKey)
+ }
+
+ // 计算分组
const grouped = {}
resources.forEach(resource => {
const docName = resource.document_name || '未知文档'
@@ -1016,10 +1145,20 @@ export default {
grouped[docName].push(resource)
})
- return Object.entries(grouped).map(([docName, items]) => ({
+ const result = Object.entries(grouped).map(([docName, items]) => ({
docName,
items
}))
+
+ // 缓存结果
+ if (this.referencesCache.size >= this.maxReferencesCacheSize) {
+ // 删除最旧的缓存条目
+ const firstKey = this.referencesCache.keys().next().value
+ this.referencesCache.delete(firstKey)
+ }
+ this.referencesCache.set(cacheKey, result)
+
+ return result
},
/**
@@ -1075,12 +1214,12 @@ export default {
this.memoryCheckInterval = setInterval(() => {
if (performance.memory) {
const memoryUsage = performance.memory.usedJSHeapSize / 1024 / 1024 // MB
-
+
// 如果内存使用超过100MB,进行清理
if (memoryUsage > 100) {
this.performMemoryCleanup()
}
-
+
this.lastMemoryUsage = memoryUsage
}
}, 30000) // 每30秒检查一次
@@ -1088,7 +1227,7 @@ export default {
// 监控DOM节点数量
this.performanceMonitor = setInterval(() => {
const messageCount = this.messages.length
-
+
// 如果消息数量过多,清理旧消息
if (messageCount > 200) {
this.cleanupOldMessages()
@@ -1104,7 +1243,7 @@ export default {
clearInterval(this.memoryCheckInterval)
this.memoryCheckInterval = null
}
-
+
if (this.performanceMonitor) {
clearInterval(this.performanceMonitor)
this.performanceMonitor = null
@@ -1118,7 +1257,7 @@ export default {
if (this.requestMonitorInterval) {
clearInterval(this.requestMonitorInterval)
}
-
+
this.requestMonitorInterval = setInterval(() => {
this.checkActiveRequests()
}, 5000) // 每5秒检查一次
@@ -1132,14 +1271,14 @@ export default {
clearInterval(this.requestMonitorInterval)
this.requestMonitorInterval = null
}
-
+
// 取消所有活跃请求
this.activeRequests.forEach((request, requestId) => {
if (request.cancel) {
request.cancel('组件销毁')
}
})
-
+
this.activeRequests.clear()
},
@@ -1149,26 +1288,26 @@ export default {
checkActiveRequests() {
const now = Date.now()
const expiredRequests = []
-
+
this.activeRequests.forEach((request, requestId) => {
const duration = now - request.startTime
-
+
// 如果请求超过最大持续时间,标记为过期
if (duration > this.maxRequestDuration) {
expiredRequests.push({ requestId, request })
}
})
-
+
// 取消过期请求
expiredRequests.forEach(({ requestId, request }) => {
console.warn(`强制取消超时请求: ${request.type}, 持续时间: ${(Date.now() - request.startTime) / 1000}秒`)
-
+
if (request.cancel) {
request.cancel('请求超时被强制取消')
}
-
+
this.activeRequests.delete(requestId)
-
+
// 显示超时提示
this.showToast('请求超时已自动取消,请重试', 'warning')
})
@@ -1181,12 +1320,12 @@ export default {
try {
// 清理引用展示状态
this.showSingleReference = {}
-
+
// 强制垃圾回收(如果浏览器支持)
if (window.gc) {
window.gc()
}
-
+
console.log('执行内存清理')
} catch (error) {
console.warn('内存清理失败:', error)
@@ -1202,15 +1341,15 @@ export default {
if (this.messages.length > 100) {
const keepCount = 100
const removedCount = this.messages.length - keepCount
-
+
this.messages = this.messages.slice(-keepCount)
-
+
// 更新最早消息ID
const userMessages = this.messages.filter(msg => msg.sender === 'user')
if (userMessages.length > 0) {
this.earliestMessageId = userMessages[0].messageId
}
-
+
console.log(`清理了 ${removedCount} 条旧消息`)
}
} catch (error) {
@@ -1224,10 +1363,10 @@ export default {
setupErrorHandling() {
// 捕获未处理的Promise错误
window.addEventListener('unhandledrejection', this.handleUnhandledRejection)
-
+
// 捕获全局JavaScript错误
window.addEventListener('error', this.handleGlobalError)
-
+
// Vue错误处理
this.$options.errorCaptured = this.handleVueError
},
@@ -1237,10 +1376,10 @@ export default {
*/
handleUnhandledRejection(event) {
console.error('未处理的Promise错误:', event.reason)
-
+
// 防止错误冒泡导致页面崩溃
event.preventDefault()
-
+
// 如果是网络错误,显示友好提示
if (event.reason && (event.reason.message || '').includes('Network')) {
this.showToast('网络连接异常,请检查网络设置', 'error')
@@ -1254,13 +1393,13 @@ export default {
*/
handleGlobalError(event) {
console.error('全局JavaScript错误:', event.error)
-
+
// 防止错误导致页面白屏
event.preventDefault()
-
+
// 如果是内存相关错误,执行清理
- if (event.error && event.error.message &&
- (event.error.message.includes('memory') || event.error.message.includes('Maximum call stack'))) {
+ if (event.error && event.error.message &&
+ (event.error.message.includes('memory') || event.error.message.includes('Maximum call stack'))) {
this.performMemoryCleanup()
this.showToast('系统资源不足,已自动清理', 'warning')
}
@@ -1271,14 +1410,14 @@ export default {
*/
handleVueError(err, instance, info) {
console.error('Vue组件错误:', err, info)
-
+
// 如果是渲染错误,尝试重置状态
if (info && info.includes('render')) {
this.$nextTick(() => {
this.$forceUpdate()
})
}
-
+
return false // 阻止错误继续传播
}
}
@@ -1972,6 +2111,28 @@ export default {
color: #722ed1 !important;
}
+/* 消息限制提示样式 */
+.message-limit-notice {
+ background: #f6f8fa;
+ border: 1px solid #e1e4e8;
+ border-radius: 8px;
+ padding: 12px 16px;
+ margin: 16px 0;
+ text-align: center;
+}
+
+.notice-text {
+ color: #586069;
+ font-size: 14px;
+ font-weight: 500;
+ margin-bottom: 4px;
+}
+
+.notice-subtext {
+ color: #959da5;
+ font-size: 12px;
+}
+
/* 响应式设计 */
@media (max-width: 768px) {
.chat-popup {
diff --git a/src/utils/ai_request.js b/src/utils/ai_request.js
index 5b387b4..b1fced2 100644
--- a/src/utils/ai_request.js
+++ b/src/utils/ai_request.js
@@ -5,7 +5,8 @@ import { showToast } from "@/utils/toast"; // 请替换为你的Toast组件
// 创建axios实例
const service = axios.create({
- baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8088',
+ // baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8088',
+ baseURL: process.env.VUE_APP_BASE_API,
timeout: 15000,
headers: {
"Content-Type": "application/json",
From 5988c81e3eacae20ac5f775bd25ec2c8fe7d9aa8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A6=85=E9=A5=BC?= <2815246336@qq.com>
Date: Tue, 19 Aug 2025 10:32:35 +0800
Subject: [PATCH 05/22] =?UTF-8?q?1.=E4=BC=98=E5=8C=96AI=E8=81=8A=E5=A4=A9?=
=?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=95=88=E6=9E=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/layout/components/Aichat/ChatPopup.vue | 141 +++++++++++++++------
1 file changed, 101 insertions(+), 40 deletions(-)
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节流,减少滚动频率
+ }
+ },
+
/**
* 处理点赞
*/
From 5a088555c867b67c0d961cc8c3f0c7945c77a467 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A6=85=E9=A5=BC?= <2815246336@qq.com>
Date: Tue, 19 Aug 2025 11:26:52 +0800
Subject: [PATCH 06/22] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=81=8A=E5=A4=A9?=
=?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/layout/components/Aichat/ChatPopup.vue | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/src/layout/components/Aichat/ChatPopup.vue b/src/layout/components/Aichat/ChatPopup.vue
index e1a9d4a..1857fb2 100644
--- a/src/layout/components/Aichat/ChatPopup.vue
+++ b/src/layout/components/Aichat/ChatPopup.vue
@@ -718,6 +718,10 @@ export default {
this.inputMessage = ''
this.sending = true
+ // 定义超时变量
+ let streamTimeout = null
+ let noDataTimeout = null
+
// 添加到请求队列
const requestId = Date.now()
this.requestQueue.push(requestId)
@@ -730,6 +734,9 @@ export default {
messageId: 'user-' + Date.now()
}
this.messages.push(userMsg)
+
+ // 添加用户消息后立即滚动到底部
+ this.scrollToBottom(false, true) // 立即滚动,强制执行
// 添加AI消息占位符
const aiMsg = {
@@ -743,7 +750,7 @@ export default {
}
this.messages.push(aiMsg)
- this.scrollToBottom(true) // 添加消息后使用平滑滚动
+ this.scrollToBottom(true, true) // 添加消息后使用平滑滚动,强制执行
try {
// 创建流式聊天
@@ -766,7 +773,7 @@ export default {
})
// 添加额外的超时保护
- const streamTimeout = setTimeout(() => {
+ streamTimeout = setTimeout(() => {
if (cancel) {
cancel('流式响应超时')
}
@@ -781,7 +788,6 @@ export default {
const { reader, decoder } = response
let buffer = ''
let lastUpdateTime = Date.now()
- let noDataTimeout = null
while (true) {
// 设置无数据超时检测
@@ -907,7 +913,7 @@ export default {
// 从监控系统中移除请求
this.activeRequests.delete(requestId)
- this.scrollToBottom(true) // 发送完成后使用平滑滚动
+ this.scrollToBottom(true, true) // 发送完成后使用平滑滚动,强制执行
}
},
From b1d7d4e59ba1387e6084257e978ed8860b5d6064 Mon Sep 17 00:00:00 2001
From: ningbo <3301955438@qq.com>
Date: Tue, 19 Aug 2025 11:30:01 +0800
Subject: [PATCH 07/22] =?UTF-8?q?fix:=20=E5=B0=86=E6=8C=89=E9=92=AE?=
=?UTF-8?q?=E6=96=87=E6=9C=AC=E4=BB=8E'=E6=9F=A5=E7=9C=8B=E6=9B=B4?=
=?UTF-8?q?=E5=A4=9A'=E6=94=B9=E4=B8=BA'=E7=82=B9=E5=87=BB=E5=89=8D?=
=?UTF-8?q?=E5=BE=80=E5=A4=84=E7=90=86'=E4=BB=A5=E6=9B=B4=E5=87=86?=
=?UTF-8?q?=E7=A1=AE=E6=8F=8F=E8=BF=B0=E6=93=8D=E4=BD=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/views/Home/index-new-blue.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/views/Home/index-new-blue.vue b/src/views/Home/index-new-blue.vue
index cfd9f9f..192c4b1 100644
--- a/src/views/Home/index-new-blue.vue
+++ b/src/views/Home/index-new-blue.vue
@@ -372,7 +372,7 @@
- 查看更多
+ 点击前往处理
From 63a39f22a75fcaf65191947938913b476e226d40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=9F=A5=E6=97=A0=E6=B6=AF?= <2637171921@qq.com>
Date: Tue, 19 Aug 2025 11:40:40 +0800
Subject: [PATCH 08/22] =?UTF-8?q?=E5=89=8D=E7=AB=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/views/Home/comps/fdy-undo.vue | 7 +++++++
src/views/Home/comps/jwc-undo.vue | 15 ++++++++++++++-
src/views/Home/comps/sj-undo.vue | 8 +++++++-
src/views/Home/comps/zdls-qgzx.vue | 12 ++++++++++++
4 files changed, 40 insertions(+), 2 deletions(-)
diff --git a/src/views/Home/comps/fdy-undo.vue b/src/views/Home/comps/fdy-undo.vue
index 843a8b8..b93186a 100644
--- a/src/views/Home/comps/fdy-undo.vue
+++ b/src/views/Home/comps/fdy-undo.vue
@@ -118,6 +118,13 @@ export default {
value: 0,
url: "/hard/zzq/fdy",
},
+ //知无涯
+ {
+ label: "辅导员·中职升高职补助审核",
+ name: "zsg",
+ value: 0,
+ url: "hard/zsg/fdy"
+ },
],
diff --git a/src/views/Home/comps/jwc-undo.vue b/src/views/Home/comps/jwc-undo.vue
index f4fb790..0fd76d9 100644
--- a/src/views/Home/comps/jwc-undo.vue
+++ b/src/views/Home/comps/jwc-undo.vue
@@ -105,7 +105,20 @@ export default {
name: "zxzm",
value: 0,
url: "/routine/school/learning"
- }
+ },
+ //知无涯 学工·中职升高职补助审核
+ {
+ label: "学工·中职升高职补助审核",
+ name: "zsg",
+ value: 0,
+ url: "/hard/zsg/xg"
+ },
+ {
+ label: "学务·辅导员综合绩效审核初审",
+ name: "jx",
+ value: 0,
+ url: "teacher/performance/evaluate/learn"
+ },
]
}
diff --git a/src/views/Home/comps/sj-undo.vue b/src/views/Home/comps/sj-undo.vue
index 74d0550..bf2d99e 100644
--- a/src/views/Home/comps/sj-undo.vue
+++ b/src/views/Home/comps/sj-undo.vue
@@ -36,7 +36,13 @@ export default {
name: "rwgl",
value: 0,
url: "/task/todo",
- }
+ },
+ {
+ label: "书记·辅导员综合绩效审核",
+ name: "jx",
+ value: 0,
+ url: "teacher/performance/evaluate/party"
+ },
]
}
diff --git a/src/views/Home/comps/zdls-qgzx.vue b/src/views/Home/comps/zdls-qgzx.vue
index ba71cef..bae19b9 100644
--- a/src/views/Home/comps/zdls-qgzx.vue
+++ b/src/views/Home/comps/zdls-qgzx.vue
@@ -70,6 +70,18 @@ export default {
value: data.workLog,
url: "/workstudy/worklog/zdls"
});
+ this.taskList.push({
+ label: "学工处长综合绩效审核",
+ name: "jx",
+ value: data.jx || 0,
+ url: "teacher/performance/studentW/director"
+ });
+ this.taskList.push({
+ label: "科室综合绩效复核",
+ name: "jx",
+ value: data.jx || 0,
+ url: "teacher/performance/studentW/department"
+ });
}
},
From 452c2b50c627ce5ef1153222b1afe21e2111b6eb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=A6=85=E9=A5=BC?= <2815246336@qq.com>
Date: Tue, 19 Aug 2025 11:47:01 +0800
Subject: [PATCH 09/22] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=95=B0=E5=AD=97?=
=?UTF-8?q?=E4=BA=BA=E5=A4=B4=E5=83=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/layout/components/Aichat/ChatPopup.vue | 364 ++++++++++++++++++++-
1 file changed, 353 insertions(+), 11 deletions(-)
diff --git a/src/layout/components/Aichat/ChatPopup.vue b/src/layout/components/Aichat/ChatPopup.vue
index 1857fb2..1dcb9ba 100644
--- a/src/layout/components/Aichat/ChatPopup.vue
+++ b/src/layout/components/Aichat/ChatPopup.vue
@@ -2,7 +2,26 @@