diff --git a/src/pages/index/components/chat/ChatMainList/index.vue b/src/pages/index/components/chat/ChatMainList/index.vue index c404855..7a116b7 100644 --- a/src/pages/index/components/chat/ChatMainList/index.vue +++ b/src/pages/index/components/chat/ChatMainList/index.vue @@ -2,55 +2,130 @@ - + - + - + - + @@ -137,7 +226,6 @@ const topNavBarRef = ref(); const welcomeRef = ref(); const notitceConent = ref(null); - const holdKeyboardTimer = ref(null); /// focus时,点击页面的时候不收起键盘 const holdKeyboard = ref(false); @@ -151,9 +239,12 @@ const scrollTop = ref(99999); /// 会话列表 const chatMsgList = ref([]); -/// 输入口的输入消息 +/// 输入框的输入消息 const inputMessage = ref(""); +/// 是否自动滚动到底部 (人工手动向上滚动时设为false) +const isAutoScroll = ref(true); + /// agentId 首页接口中获取 const agentId = ref("1"); /// 会话ID 历史数据接口中获取 @@ -169,7 +260,12 @@ let messageCommonType = ""; // WebSocket 相关 let webSocketManager = null; /// 使用统一的连接状态判断函数,避免状态不同步 -const isWsConnected = () => !!(webSocketManager && typeof webSocketManager.isConnected === "function" && webSocketManager.isConnected()); +const isWsConnected = () => + !!( + webSocketManager && + typeof webSocketManager.isConnected === "function" && + webSocketManager.isConnected() + ); // pendingMap: messageId -> msgIndex const pendingMap = new Map(); @@ -208,7 +304,7 @@ const handleKeyboardShow = () => { holdKeyboard.value = true; // 键盘弹起时调整聊天内容的底部边距并滚动到底部 setTimeout(() => { - scrollToBottom(); + scrollToBottom(true); }, 150); }; @@ -220,45 +316,66 @@ const handleKeyboardHide = () => { // 处理用户滚动事件 const welcomeHeight = ref(0); -const handleScroll = ThrottleUtils.createThrottle(({ detail }) => { +let lastScrollTop = 0; +const handleScroll = (e) => { + const detail = e.detail; topNavBarRef.value.show = parseInt(detail.scrollTop) > welcomeHeight.value; -}, 50); + + const currentScrollTop = detail.scrollTop; + // 如果向上滚动 (当前位置小于上一次记录的位置) + if (currentScrollTop < lastScrollTop - 2) { + // 增加 2px 阈值防止抖动 + isAutoScroll.value = false; + } + lastScrollTop = currentScrollTop; +}; // 处理滚动到底部事件 -const handleScrollToLower = () => { }; +const handleScrollToLower = () => { + // 当用户滚动到底部时,明确开启自动滚动 + isAutoScroll.value = true; +}; // 滚动到底部 - 优化版本,确保打字机效果始终可见 -const scrollToBottom = () => { +const scrollToBottom = (force = false) => { + // 如果当前不是自动滚动模式且非强制触发,则不执行滚动 + if (!isAutoScroll.value && !force) { + console.log("暂停自动滚动,当前位置:", scrollTop.value); + return; + } + nextTick(() => { // 使用更大的值确保滚动到真正的底部 - scrollTop.value = 99999; - // 强制触发滚动更新,增加延迟确保DOM更新完成 - setTimeout(() => { - scrollTop.value = scrollTop.value + Math.random(); - }, 10); + const targetScrollTop = 99999 + Math.random(); + scrollTop.value = targetScrollTop; }); }; // 延时滚动 -const setTimeoutScrollToBottom = () => setTimeout(() => scrollToBottom(), 100); +const setTimeoutScrollToBottom = (force = false) => + setTimeout(() => scrollToBottom(force), 100); // 发送普通消息 const handleReplyText = (text) => { + // 发送消息时,强制开启自动滚动 + isAutoScroll.value = true; // 重置消息状态,准备接收新的AI回复 resetMessageState(); sendMessage(text); - setTimeoutScrollToBottom(); + setTimeoutScrollToBottom(true); }; // 是发送指令消息 const handleReplyInstruct = async (item) => { await checkToken(); + // 发送消息时,强制开启自动滚动 + isAutoScroll.value = true; messageCommonType = item.type; // 重置消息状态,准备接收新的AI回复 resetMessageState(); sendMessage(item.title, true); - setTimeoutScrollToBottom(); + setTimeoutScrollToBottom(true); }; // 输入区的发送消息事件 @@ -267,6 +384,8 @@ const sendMessageAction = (inputText) => { if (!inputText.trim()) return; handleNoHideKeyboard(); + // 发送消息时,强制开启自动滚动 + isAutoScroll.value = true; // 重置消息状态,准备接收新的AI回复 resetMessageState(); @@ -278,7 +397,7 @@ const sendMessageAction = (inputText) => { }, 100); } - setTimeoutScrollToBottom(); + setTimeoutScrollToBottom(true); }; /// 添加通知 @@ -295,7 +414,7 @@ const addNoticeListener = () => { uni.$on(SCROLL_TO_BOTTOM, () => { setTimeout(() => { - scrollToBottom(); + scrollToBottom(true); }, 200); }); @@ -425,11 +544,14 @@ const initWebSocket = async () => { // 连接成功后发送 welcome 消息 (messageType=4) try { // fire-and-forget, sendWebSocketMessage 会处理重连与队列 - sendWebSocketMessage(MessageType.notice, Command.messageInit, { tryReconnect: true, messageId:IdUtils.generateMessageId() }).catch((e) => { - console.warn('发送 Command.messageInit 消息失败:', e); + sendWebSocketMessage(MessageType.notice, Command.messageInit, { + tryReconnect: true, + messageId: IdUtils.generateMessageId(), + }).catch((e) => { + console.warn("发送 Command.messageInit 消息失败:", e); }); } catch (e) { - console.warn('发送 Command.messageInit 消息时异常:', e); + console.warn("发送 Command.messageInit 消息时异常:", e); } }, @@ -487,7 +609,7 @@ const handleWebSocketMessage = (data) => { return; } - if (data.messageType && data.messageType === 'broadcast') { + if (data.messageType && data.messageType === "broadcast") { console.log("收到 welcome 类型消息:", data); if (data.content) { notitceConent.value = data.content; @@ -501,26 +623,39 @@ const handleWebSocketMessage = (data) => { // 1) Try to find an existing AI message that already has the same replyMessageId for (let i = chatMsgList.value.length - 1; i >= 0; i--) { const it = chatMsgList.value[i]; - if (it && it.msgType === MessageRole.AI && it.replyMessageId === data.replyMessageId) { + if ( + it && + it.msgType === MessageRole.AI && + it.replyMessageId === data.replyMessageId + ) { aiMsgIndex = i; break; } } // 2) If not found, check pendingMap for currentSessionMessageId - if (aiMsgIndex === -1 && currentSessionMessageId && pendingMap.has(currentSessionMessageId)) { + if ( + aiMsgIndex === -1 && + currentSessionMessageId && + pendingMap.has(currentSessionMessageId) + ) { const idx = pendingMap.get(currentSessionMessageId); if (idx >= 0 && idx < chatMsgList.value.length) { const item = chatMsgList.value[idx]; // If the pending item already has a different non-empty replyMessageId, create a new AI entry - if (item && item.msgType === MessageRole.AI && item.replyMessageId && item.replyMessageId !== data.replyMessageId) { + if ( + item && + item.msgType === MessageRole.AI && + item.replyMessageId && + item.replyMessageId !== data.replyMessageId + ) { const aiMsg = { msgId: `msg_${chatMsgList.value.length}`, msgType: MessageRole.AI, msg: "", isLoading: false, messageId: currentSessionMessageId, - replyMessageId: data.replyMessageId || '', + replyMessageId: data.replyMessageId || "", componentName: "", title: "", finish: false, @@ -542,7 +677,7 @@ const handleWebSocketMessage = (data) => { msg: "", isLoading: false, messageId: currentSessionMessageId, - replyMessageId: data.replyMessageId || '', + replyMessageId: data.replyMessageId || "", componentName: "", title: "", finish: false, @@ -553,7 +688,10 @@ const handleWebSocketMessage = (data) => { } else { // No replyMessageId: fall back to most recent AI message for (let i = chatMsgList.value.length - 1; i >= 0; i--) { - if (chatMsgList.value[i] && chatMsgList.value[i].msgType === MessageRole.AI) { + if ( + chatMsgList.value[i] && + chatMsgList.value[i].msgType === MessageRole.AI + ) { aiMsgIndex = i; break; } @@ -566,12 +704,12 @@ const handleWebSocketMessage = (data) => { // 防护:确保 aiMsgIndex 有效 if (aiMsgIndex < 0 || aiMsgIndex >= chatMsgList.value.length) { - console.error('无效的 aiMsgIndex:', aiMsgIndex); + console.error("无效的 aiMsgIndex:", aiMsgIndex); return; } // replyMessageId - if(data.replyMessageId) { + if (data.replyMessageId) { chatMsgList.value[aiMsgIndex].replyMessageId = data.replyMessageId; } @@ -599,7 +737,9 @@ const handleWebSocketMessage = (data) => { // 直接拼接内容到对应 AI 消息 if (data.content) { // 如果该条消息属于 longTextCard,使用 componentMsg 存储内容并保持 ChatCardAI 的 text 为空 - const isLongText = aiItem.componentName === CompName.longTextCard || data.componentName === CompName.longTextCard; + const isLongText = + aiItem.componentName === CompName.longTextCard || + data.componentName === CompName.longTextCard; if (isLongText) { if (aiItem.isLoading) { aiItem.componentMsg = (aiItem.componentMsg || "") + data.content; @@ -703,7 +843,7 @@ const sendMessage = async (message, isInstruct = false) => { try { await initWebSocket(); // 等待短暂时间确保连接建立 - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); // 检查连接是否成功建立 if (!isWsConnected()) { @@ -742,13 +882,17 @@ const sendMessage = async (message, isInstruct = false) => { chatMsgList.value.push(newMsg); inputMessage.value = ""; // 发送消息后滚动到底部 - setTimeoutScrollToBottom(); + setTimeoutScrollToBottom(true); sendChat(message, isInstruct); console.log("发送的新消息:", JSON.stringify(newMsg)); }; // 通用WebSocket消息发送函数 -> 返回 Promise -const sendWebSocketMessage = async (messageType, messageContent, options = {}) => { +const sendWebSocketMessage = async ( + messageType, + messageContent, + options = {}, +) => { const args = { conversationId: conversationId.value, agentId: agentId.value, @@ -757,9 +901,11 @@ const sendWebSocketMessage = async (messageType, messageContent, options = {}) = messageId: options.messageId || currentSessionMessageId, }; - const maxRetries = typeof options.retries === 'number' ? options.retries : 3; - const baseDelay = typeof options.baseDelay === 'number' ? options.baseDelay : 300; // ms - const maxDelay = typeof options.maxDelay === 'number' ? options.maxDelay : 5000; // ms + const maxRetries = typeof options.retries === "number" ? options.retries : 3; + const baseDelay = + typeof options.baseDelay === "number" ? options.baseDelay : 300; // ms + const maxDelay = + typeof options.maxDelay === "number" ? options.maxDelay : 5000; // ms for (let attempt = 0; attempt <= maxRetries; attempt++) { // 确保连接 @@ -768,13 +914,13 @@ const sendWebSocketMessage = async (messageType, messageContent, options = {}) = try { await initWebSocket(); } catch (e) { - console.error('reconnect failed in sendWebSocketMessage:', e); + console.error("reconnect failed in sendWebSocketMessage:", e); } } } if (!isWsConnected()) { - if (!options.silent) console.warn('WebSocket 未连接,无法发送消息', args); + if (!options.silent) console.warn("WebSocket 未连接,无法发送消息", args); // 如果还有重试机会,进行等待后重试 if (attempt < maxRetries) { const delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt)); @@ -796,25 +942,41 @@ const sendWebSocketMessage = async (messageType, messageContent, options = {}) = // 若返回 false,消息可能已经被 manager 入队并触发连接流程。 // 在这种情况下避免立即当作失败处理,而是等待短暂时间以观察连接是否建立并由 manager 发送队列。 - console.warn('webSocketManager.sendMessage 返回 false,等待连接或队列发送...', { attempt, args }); - const waitForConnectMs = typeof options.waitForConnectMs === 'number' ? options.waitForConnectMs : 5000; - if (webSocketManager && typeof webSocketManager.isConnected === 'function' && !webSocketManager.isConnected()) { + console.warn( + "webSocketManager.sendMessage 返回 false,等待连接或队列发送...", + { attempt, args }, + ); + const waitForConnectMs = + typeof options.waitForConnectMs === "number" + ? options.waitForConnectMs + : 5000; + if ( + webSocketManager && + typeof webSocketManager.isConnected === "function" && + !webSocketManager.isConnected() + ) { const startTs = Date.now(); while (Date.now() - startTs < waitForConnectMs) { await sleep(200); if (webSocketManager.isConnected()) { // 给 manager 一点时间处理队列并发送 await sleep(150); - console.log('检测到 manager 已连接,假定队列消息已发送', args); + console.log("检测到 manager 已连接,假定队列消息已发送", args); return true; } } - console.warn('等待 manager 建连超时,进入重试逻辑', { waitForConnectMs, args }); + console.warn("等待 manager 建连超时,进入重试逻辑", { + waitForConnectMs, + args, + }); } else { - console.warn('sendMessage 返回 false 但 manager 看起来已连接或不可用,继续重试', { args }); + console.warn( + "sendMessage 返回 false 但 manager 看起来已连接或不可用,继续重试", + { args }, + ); } } catch (error) { - console.error('发送WebSocket消息异常:', error, args); + console.error("发送WebSocket消息异常:", error, args); } // 失败且还有重试机会,等待指数退避 @@ -845,8 +1007,13 @@ const sendChat = async (message, isInstruct = false) => { isSessionActive.value = connected; // 更新AI消息状态 const aiMsgIndex = chatMsgList.value.length - 1; - if (aiMsgIndex >= 0 && chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI) { - chatMsgList.value[aiMsgIndex].msg = connected ? "" : "发送消息失败,请重试"; + if ( + aiMsgIndex >= 0 && + chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI + ) { + chatMsgList.value[aiMsgIndex].msg = connected + ? "" + : "发送消息失败,请重试"; chatMsgList.value[aiMsgIndex].isLoading = connected; } if (connected) { @@ -871,14 +1038,14 @@ const sendChat = async (message, isInstruct = false) => { msg: "思考中", isLoading: true, messageId: currentSessionMessageId, - replyMessageId: '', + replyMessageId: "", componentName: "", title: "", finish: false, }; chatMsgList.value.push(aiMsg); // 添加AI消息后滚动到底部 - setTimeoutScrollToBottom(); + setTimeoutScrollToBottom(true); const aiMsgIndex = chatMsgList.value.length - 1; // 记录 pendingMap @@ -887,19 +1054,26 @@ const sendChat = async (message, isInstruct = false) => { // 启动超时回退 const timeoutId = setTimeout(() => { const idx = pendingMap.get(currentSessionMessageId); - if (idx != null && chatMsgList.value[idx] && chatMsgList.value[idx].isLoading) { + if ( + idx != null && + chatMsgList.value[idx] && + chatMsgList.value[idx].isLoading + ) { chatMsgList.value[idx].msg = "请求超时,请重试"; chatMsgList.value[idx].isLoading = false; pendingMap.delete(currentSessionMessageId); pendingTimeouts.delete(currentSessionMessageId); isSessionActive.value = false; - setTimeoutScrollToBottom(); + setTimeoutScrollToBottom(true); } }, MESSAGE_TIMEOUT); pendingTimeouts.set(currentSessionMessageId, timeoutId); // 发送消息,支持重连尝试 - const success = await sendWebSocketMessage(messageType, messageContent, { messageId: currentSessionMessageId, tryReconnect: true }); + const success = await sendWebSocketMessage(messageType, messageContent, { + messageId: currentSessionMessageId, + tryReconnect: true, + }); if (!success) { const idx = pendingMap.get(currentSessionMessageId); if (idx != null && chatMsgList.value[idx]) { @@ -922,7 +1096,10 @@ const stopRequest = async () => { // 发送中断消息给服务器 (messageType=2),带上当前 messageId try { - await sendWebSocketMessage(MessageType.stop, "stop_request", { messageId: currentSessionMessageId, silent: true }); + await sendWebSocketMessage(MessageType.stop, "stop_request", { + messageId: currentSessionMessageId, + silent: true, + }); } catch (e) { console.warn("stopRequest send failed:", e); } @@ -935,12 +1112,16 @@ const stopRequest = async () => { aiMsgIndex = chatMsgList.value.length - 1; } - if (chatMsgList.value[aiMsgIndex] && - chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI) { + if ( + chatMsgList.value[aiMsgIndex] && + chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI + ) { chatMsgList.value[aiMsgIndex].isLoading = false; - if (chatMsgList.value[aiMsgIndex].msg && + if ( + chatMsgList.value[aiMsgIndex].msg && chatMsgList.value[aiMsgIndex].msg.trim() && - !chatMsgList.value[aiMsgIndex].msg.startsWith("思考中")) { + !chatMsgList.value[aiMsgIndex].msg.startsWith("思考中") + ) { // 保留已显示内容 } else { chatMsgList.value[aiMsgIndex].msg = "请求已停止"; @@ -958,7 +1139,7 @@ const stopRequest = async () => { // 重置会话状态 isSessionActive.value = false; - setTimeoutScrollToBottom(); + setTimeoutScrollToBottom(true); }; // 组件销毁时清理资源