Files
YGChatCS/src/pages/index/components/chat/ChatMainList/index.vue

880 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="flex flex-col h-screen">
<!-- 顶部自定义导航栏 -->
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
<ChatTopNavBar ref="topNavBarRef" :mainPageDataModel="mainPageDataModel" />
</view>
<!-- 消息列表可滚动区域 -->
<scroll-view class="main flex-full overflow-hidden scroll-y" scroll-y :scroll-top="scrollTop"
:scroll-with-animation="true" @scroll="handleScroll" @scrolltolower="handleScrollToLower">
<!-- welcome栏 -->
<ChatTopWelcome ref="welcomeRef" :mainPageDataModel="mainPageDataModel" />
<view class="area-msg-list-content" v-for="item in chatMsgList" :key="item.msgId" :id="item.msgId">
<template v-if="item.msgType === MessageRole.AI">
<ChatCardAI class="flex flex-justify-start" :key="`ai-${item.msgId}-${item.msg ? item.msg.length : 0}`"
:text="item.msg || ''" :isLoading="item.isLoading">
<template #content v-if="item.toolCall">
<QuickBookingComponent v-if="item.toolCall.componentName === CompName.quickBookingCard" />
<DiscoveryCardComponent v-else-if="
item.toolCall.componentName === CompName.discoveryCard
" />
<CreateServiceOrder v-else-if="
item.toolCall.componentName === CompName.callServiceCard
" :toolCall="item.toolCall" />
<Feedback v-else-if="
item.toolCall.componentName === CompName.feedbackCard
" :toolCall="item.toolCall" />
<DetailCardCompontent v-else-if="
item.toolCall.componentName ===
CompName.pictureAndCommodityCard
" :toolCall="item.toolCall" />
<AddCarCrad v-else-if="
item.toolCall.componentName === CompName.enterLicensePlateCard
" :toolCall="item.toolCall" />
</template>
<template #footer>
<!-- 这个是底部 -->
<AttachListComponent v-if="item.question" :question="item.question" />
</template>
</ChatCardAI>
</template>
<template v-else-if="item.msgType === MessageRole.ME">
<ChatCardMine class="flex flex-justify-end" :text="item.msg" />
</template>
<template v-else>
<ChatCardOther :text="item.msg">
<ActivityListComponent v-if="
mainPageDataModel.activityList &&
mainPageDataModel.activityList.length > 0
" :activityList="mainPageDataModel.activityList" />
<RecommendPostsComponent v-if="
mainPageDataModel.recommendTheme &&
mainPageDataModel.recommendTheme.length > 0
" :recommendThemeList="mainPageDataModel.recommendTheme" />
</ChatCardOther>
</template>
</view>
</scroll-view>
<!-- 输入框区域 -->
<view class="pb-safe-area">
<ChatQuickAccess />
<ChatInputArea ref="inputAreaRef" v-model="inputMessage" :holdKeyboard="holdKeyboard"
:is-session-active="isSessionActive" :stop-request="stopRequest" @send="sendMessageAction"
@noHideKeyboard="handleNoHideKeyboard" @keyboardShow="handleKeyboardShow" @keyboardHide="handleKeyboardHide" />
</view>
</view>
</template>
<script setup>
import { onMounted, nextTick, onUnmounted, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import {
SCROLL_TO_BOTTOM,
SEND_MESSAGE_CONTENT_TEXT,
SEND_MESSAGE_COMMAND_TYPE,
NOTICE_EVENT_LOGOUT,
NOTICE_EVENT_LOGIN_SUCCESS,
} from "@/constant/constant";
import { MessageRole, MessageType, CompName } from "@/model/ChatModel";
import ChatTopWelcome from "../ChatTopWelcome/index.vue";
import ChatTopNavBar from "../ChatTopNavBar/index.vue";
import ChatCardAI from "../ChatCardAi/index.vue";
import ChatCardMine from "../ChatCardMine/index.vue";
import ChatCardOther from "../ChatCardOther/index.vue";
import ChatQuickAccess from "../ChatQuickAccess/index.vue";
import ChatInputArea from "../ChatInputArea/index.vue";
import QuickBookingComponent from "../../module/QuickBookingComponent/index.vue";
import DiscoveryCardComponent from "../../module/DiscoveryCardComponent/index.vue";
import ActivityListComponent from "../../module/ActivityListComponent/index.vue";
import RecommendPostsComponent from "../../module/RecommendPostsComponent/index.vue";
import AttachListComponent from "../../module/AttachListComponent/index.vue";
import DetailCardCompontent from "../../module/DetailCardCompontent/index.vue";
import CreateServiceOrder from "@/components/CreateServiceOrder/index.vue";
import Feedback from "@/components/Feedback/index.vue";
import AddCarCrad from "@/components/AddCarCrad/index.vue";
import { mainPageData } from "@/request/api/MainPageDataApi";
import {
conversationMsgList,
recentConversation,
} from "@/request/api/ConversationApi";
import WebSocketManager from "@/utils/WebSocketManager";
import { ThrottleUtils, IdUtils } from "@/utils";
import { checkToken } from "@/hooks/useGoLogin";
import { useAppStore } from "@/store";
import { getAccessToken } from "@/constant/token";
const appStore = useAppStore();
/// 导航栏相关
const statusBarHeight = ref(20);
/// 输入框组件引用
const inputAreaRef = ref(null);
const topNavBarRef = ref();
const welcomeRef = ref();
const holdKeyboardTimer = ref(null);
/// focus时点击页面的时候不收起键盘
const holdKeyboard = ref(false);
/// 是否在键盘弹出,点击界面时关闭键盘
const holdKeyboardFlag = ref(true);
/// 是否显示键盘
const isKeyboardShow = ref(false);
///(控制滚动位置)
const scrollTop = ref(99999);
/// 会话列表
const chatMsgList = ref([]);
/// 输入口的输入消息
const inputMessage = ref("");
/// agentId 首页接口中获取
const agentId = ref("1");
/// 会话ID 历史数据接口中获取
const conversationId = ref("");
/// 首页的数据
const mainPageDataModel = ref({});
// 会话进行中标志
const isSessionActive = ref(false);
/// 指令
let commonType = "";
// WebSocket 相关
let webSocketManager = null;
/// 使用统一的连接状态判断函数,避免状态不同步
const isWsConnected = () => !!(webSocketManager && typeof webSocketManager.isConnected === "function" && webSocketManager.isConnected());
// pendingMap: messageId -> msgIndex
const pendingMap = new Map();
// pendingTimeouts: messageId -> timeoutId
const pendingTimeouts = new Map();
// 超时时间ms
const MESSAGE_TIMEOUT = 30000;
// 防止并发初始化 websocket
let isInitializing = false;
let pendingInitPromise = null;
// sleep helper
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
// 当前会话的消息ID用于保持发送和终止的messageId一致
let currentSessionMessageId = null;
/// =============事件函数↓================
const handleTouchEnd = () => {
clearTimeout(holdKeyboardTimer.value);
holdKeyboardTimer.value = setTimeout(() => {
// 键盘弹出时点击界面则关闭键盘
if (holdKeyboardFlag.value && isKeyboardShow.value) {
uni.hideKeyboard();
}
holdKeyboardFlag.value = true;
}, 100);
};
// 点击输入框、发送按钮时,不收键盘
const handleNoHideKeyboard = () => (holdKeyboardFlag.value = false);
// 键盘弹起事件
const handleKeyboardShow = () => {
isKeyboardShow.value = true;
holdKeyboard.value = true;
// 键盘弹起时调整聊天内容的底部边距并滚动到底部
setTimeout(() => {
scrollToBottom();
}, 150);
};
// 键盘收起事件
const handleKeyboardHide = () => {
isKeyboardShow.value = false;
holdKeyboard.value = false;
};
// 处理用户滚动事件
const welcomeHeight = ref(0);
const handleScroll = ThrottleUtils.createThrottle(({ detail }) => {
topNavBarRef.value.show = parseInt(detail.scrollTop) > welcomeHeight.value;
}, 50);
// 处理滚动到底部事件
const handleScrollToLower = () => { };
// 滚动到底部 - 优化版本,确保打字机效果始终可见
const scrollToBottom = () => {
nextTick(() => {
// 使用更大的值确保滚动到真正的底部
scrollTop.value = 99999;
// 强制触发滚动更新增加延迟确保DOM更新完成
setTimeout(() => {
scrollTop.value = scrollTop.value + Math.random();
}, 10);
});
};
// 延时滚动
const setTimeoutScrollToBottom = () => setTimeout(() => scrollToBottom(), 100);
// 发送普通消息
const handleReplyText = (text) => {
// 重置消息状态准备接收新的AI回复
resetMessageState();
sendMessage(text);
setTimeoutScrollToBottom();
};
// 是发送指令消息
const handleReplyInstruct = async (item) => {
await checkToken();
commonType = item.type;
// 重置消息状态准备接收新的AI回复
resetMessageState();
sendMessage(item.title, true);
setTimeoutScrollToBottom();
};
// 输入区的发送消息事件
const sendMessageAction = (inputText) => {
console.log("输入消息:", inputText);
if (!inputText.trim()) return;
handleNoHideKeyboard();
// 重置消息状态准备接收新的AI回复
resetMessageState();
sendMessage(inputText);
// 发送消息后保持键盘状态
if (holdKeyboard.value && inputAreaRef.value) {
setTimeout(() => {
inputAreaRef.value.focusInput();
}, 100);
}
setTimeoutScrollToBottom();
};
/// 添加通知
const addNoticeListener = () => {
uni.$on(NOTICE_EVENT_LOGIN_SUCCESS, () => {
if (!isWsConnected()) {
initHandler();
}
});
uni.$on(NOTICE_EVENT_LOGOUT, () => {
resetConfig();
});
uni.$on(SCROLL_TO_BOTTOM, () => {
setTimeout(() => {
scrollToBottom();
}, 200);
});
uni.$on(SEND_MESSAGE_CONTENT_TEXT, (value) => {
console.log("SEND_MESSAGE_CONTENT_TEXT:", value);
if (value && value.length > 0) {
handleReplyText(value);
}
});
uni.$on(SEND_MESSAGE_COMMAND_TYPE, (item) => {
console.log("SEND_MESSAGE_COMMAND_TYPE:", item);
if (item && item.type) {
handleReplyInstruct(item);
}
});
};
/// =============生命周期函数↓================
onLoad(() => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20;
},
});
});
// token存在初始化数据
const initHandler = () => {
console.log("initHandler");
const token = getAccessToken();
if (!token) return;
loadRecentConversation();
///loadConversationMsgList();
initWebSocket();
};
onMounted(() => {
try {
getMainPageData();
addNoticeListener();
// 有token时加载最近会话、最近消息、初始化socket
initHandler();
} catch (error) {
console.error("页面初始化错误:", error);
}
});
/// =============页面配置↓================
// 获取最近一次的会话id
const loadRecentConversation = async () => {
const res = await recentConversation();
if (res.code === 0) {
conversationId.value = res.data.conversationId;
}
};
// 加载历史消息的数据
let historyCurrentPageNum = 1;
const loadConversationMsgList = async () => {
const args = {
pageNum: historyCurrentPageNum++,
pageSize: 10,
conversationId: conversationId.value,
};
const res = await conversationMsgList(args);
};
// 获取首页数据
const getMainPageData = async () => {
/// 从个渠道获取如二维,没有的时候就返回首页的数据
const sceneId = appStore.sceneId || "";
const res = await mainPageData({ sceneId });
if (res.code === 0) {
initData();
mainPageDataModel.value = res.data;
agentId.value = res.data.agentId;
// 数据更新后再次测量欢迎区高度
welcomeRef.value.measureWelcomeHeight((data) => {
console.log("🚀 ~ getMainPageData ~ data:", data);
welcomeHeight.value = data.height;
});
}
appStore.setSceneId(""); // 清空sceneId,分身二次进入展示默认的
};
/// =============对话↓================
// 初始化WebSocket
const initWebSocket = async () => {
// 防止并发初始化
if (isInitializing) {
return pendingInitPromise;
}
isInitializing = true;
pendingInitPromise = (async () => {
// 清理旧实例
if (webSocketManager) {
try {
webSocketManager.destroy();
} catch (e) {
console.warn("destroy old webSocketManager failed:", e);
}
webSocketManager = null;
}
// 使用配置的WebSocket服务器地址
const token = getAccessToken();
const wsUrl = `${appStore.serverConfig.wssUrl}?access_token=${token}`;
// 初始化WebSocket管理器
webSocketManager = new WebSocketManager({
wsUrl: wsUrl,
reconnectInterval: 3000, // 重连间隔
maxReconnectAttempts: 5, // 最大重连次数
heartbeatInterval: 30000, // 心跳间隔
// 连接成功回调
onOpen: (event) => {
console.log("WebSocket连接成功");
// 重置会话状态
isSessionActive.value = false; // 连接成功时重置会话状态,避免影响新消息发送
},
// 连接断开回调
onClose: (event) => {
console.error("WebSocket连接断开:", event);
// 停止当前会话
isSessionActive.value = false;
},
// 错误回调
onError: (error) => {
console.error("WebSocket错误:", error);
isSessionActive.value = false;
},
// 消息回调
onMessage: (data) => {
handleWebSocketMessage(data);
},
// 获取会话ID回调 (用于心跳检测)
getConversationId: () => conversationId.value,
// 获取代理ID回调 (用于心跳检测)
getAgentId: () => agentId.value,
});
try {
// 初始化连接
await webSocketManager.connect();
console.log("WebSocket连接初始化成功");
return true;
} catch (error) {
console.error("WebSocket连接失败:", error);
return false;
} finally {
isInitializing = false;
pendingInitPromise = null;
}
})();
return pendingInitPromise;
};
// 处理WebSocket消息
const handleWebSocketMessage = (data) => {
// 验证关键字段(若服务端传回 conversationId/agentId则校验是否属于当前会话
if (data.conversationId && data.conversationId !== conversationId.value) {
console.warn("收到不属于当前会话的消息,忽略", data.conversationId);
return;
}
if (data.agentId && data.agentId !== agentId.value) {
console.warn("收到不属于当前 agent 的消息,忽略", data.agentId);
return;
}
// 确保消息内容是字符串类型
if (data.content && typeof data.content !== "string") {
try {
data.content = JSON.stringify(data.content);
} catch (e) {
data.content = String(data.content);
}
}
// 优先使用 messageId 进行匹配
const msgId = data.messageId || data.id || data.msgId;
let aiMsgIndex = -1;
if (msgId && pendingMap.has(msgId)) {
aiMsgIndex = pendingMap.get(msgId);
} else if (!msgId && currentSessionMessageId && pendingMap.has(currentSessionMessageId)) {
// 服务端未返回 messageId 的场景:优先使用当前会话的 messageId 映射
aiMsgIndex = pendingMap.get(currentSessionMessageId);
} else {
// 向后搜索最近的 AI 消息
for (let i = chatMsgList.value.length - 1; i >= 0; i--) {
if (chatMsgList.value[i] && chatMsgList.value[i].msgType === MessageRole.AI) {
aiMsgIndex = i;
break;
}
}
if (aiMsgIndex === -1) {
console.error("处理WebSocket消息时找不到对应的AI消息项");
return;
}
}
// 直接拼接内容到对应 AI 消息
if (data.content) {
if (chatMsgList.value[aiMsgIndex].isLoading) {
// 首次收到内容:替换“加载中”文案并取消 loading 状态(恢复原始渲染逻辑)
chatMsgList.value[aiMsgIndex].msg = data.content;
chatMsgList.value[aiMsgIndex].isLoading = false;
} else {
// 后续流式内容追加
chatMsgList.value[aiMsgIndex].msg += data.content;
}
nextTick(() => scrollToBottom());
}
// 处理完成状态
if (data.finish) {
const msg = chatMsgList.value[aiMsgIndex].msg;
if (!msg || chatMsgList.value[aiMsgIndex].isLoading) {
chatMsgList.value[aiMsgIndex].msg = "未获取到内容,请重试";
chatMsgList.value[aiMsgIndex].isLoading = false;
if (data.toolCall) {
chatMsgList.value[aiMsgIndex].msg = "";
}
}
// 处理toolCall
if (data.toolCall) {
chatMsgList.value[aiMsgIndex].toolCall = data.toolCall;
}
// 处理question
if (data.question && data.question.length > 0) {
chatMsgList.value[aiMsgIndex].question = data.question;
}
// 清理 pendingMap / timeout
const ownedMessageId = chatMsgList.value[aiMsgIndex].messageId || msgId;
if (ownedMessageId) {
if (pendingTimeouts.has(ownedMessageId)) {
clearTimeout(pendingTimeouts.get(ownedMessageId));
pendingTimeouts.delete(ownedMessageId);
}
pendingMap.delete(ownedMessageId);
}
// 重置会话状态
isSessionActive.value = false;
// 清理当前会话的 messageId避免保留陈旧 id
resetMessageState();
}
};
// 重置消息状态
const resetMessageState = () => {
// 重置当前会话消息ID
currentSessionMessageId = null;
};
// 初始化数据 首次数据加载的时候
const initData = () => {
const msg = {
msgId: `msg_${0}`,
msgType: MessageRole.OTHER,
msg: "",
};
chatMsgList.value.push(msg);
};
// 发送消息的参数拼接
const sendMessage = async (message, isInstruct = false) => {
console.log("发送的消息:", message);
await checkToken();
// 检查WebSocket连接状态如果未连接尝试重新连接
if (!isWsConnected()) {
console.log("WebSocket未连接尝试重新连接...");
// 显示加载提示
uni.showLoading({
title: "正在连接服务器...",
});
// 尝试重新初始化WebSocket连接
try {
await initWebSocket();
// 等待短暂时间确保连接建立
await new Promise(resolve => setTimeout(resolve, 1000));
// 检查连接是否成功建立
if (!isWsConnected()) {
uni.hideLoading();
uni.showToast({
title: "连接服务器失败,请稍后重试",
icon: "none",
});
return;
}
uni.hideLoading();
} catch (error) {
console.error("重新连接WebSocket失败:", error);
uni.hideLoading();
uni.showToast({
title: "连接服务器失败,请稍后重试",
icon: "none",
});
return;
}
}
if (isSessionActive.value) {
uni.showToast({
title: "请等待当前回复完成",
icon: "none",
});
return;
}
isSessionActive.value = true;
const newMsg = {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.ME,
msg: message,
msgContent: {
type: MessageType.TEXT,
text: message,
},
};
chatMsgList.value.push(newMsg);
inputMessage.value = "";
// 发送消息后滚动到底部
setTimeoutScrollToBottom();
sendChat(message, isInstruct);
console.log("发送的新消息:", JSON.stringify(newMsg));
};
// 通用WebSocket消息发送函数 -> 返回 Promise<boolean>
const sendWebSocketMessage = async (messageType, messageContent, options = {}) => {
const args = {
conversationId: conversationId.value,
agentId: agentId.value,
messageType: String(messageType), // 消息类型 0-对话 1-指令 2-中断停止 3-心跳检测
messageContent: messageContent,
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
for (let attempt = 0; attempt <= maxRetries; attempt++) {
// 确保连接
if (!isWsConnected()) {
if (options.tryReconnect) {
try {
await initWebSocket();
} catch (e) {
console.error('reconnect failed in sendWebSocketMessage:', e);
}
}
}
if (!isWsConnected()) {
if (!options.silent) console.warn('WebSocket 未连接,无法发送消息', args);
// 如果还有重试机会,进行等待后重试
if (attempt < maxRetries) {
const delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
await sleep(delay);
continue;
}
isSessionActive.value = false;
return false;
}
try {
const raw = webSocketManager.sendMessage(args);
// 兼容可能返回同步布尔或 Promise 的实现
const result = await Promise.resolve(raw);
if (result) {
console.log(`WebSocket消息已发送 [类型:${messageType}]:`, args);
return true;
}
// 若返回 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()) {
const startTs = Date.now();
while (Date.now() - startTs < waitForConnectMs) {
await sleep(200);
if (webSocketManager.isConnected()) {
// 给 manager 一点时间处理队列并发送
await sleep(150);
console.log('检测到 manager 已连接,假定队列消息已发送', args);
return true;
}
}
console.warn('等待 manager 建连超时,进入重试逻辑', { waitForConnectMs, args });
} else {
console.warn('sendMessage 返回 false 但 manager 看起来已连接或不可用,继续重试', { args });
}
} catch (error) {
console.error('发送WebSocket消息异常:', error, args);
}
// 失败且还有重试机会,等待指数退避
if (attempt < maxRetries) {
const delay = Math.min(maxDelay, baseDelay * Math.pow(2, attempt));
await sleep(delay + Math.floor(Math.random() * 100));
continue;
}
// 最后一次失败
isSessionActive.value = false;
return false;
}
// 不可达,但为了类型安全
isSessionActive.value = false;
return false;
};
// 发送获取AI聊天消息
const sendChat = async (message, isInstruct = false) => {
// 检查WebSocket管理器是否存在如果不存在尝试重新初始化
if (!webSocketManager) {
console.error("WebSocket管理器不存在尝试重新初始化...");
initWebSocket();
// 短暂延迟后再次检查连接状态
setTimeout(() => {
const connected = webSocketManager && webSocketManager.isConnected();
isSessionActive.value = connected;
// 更新AI消息状态
const aiMsgIndex = chatMsgList.value.length - 1;
if (aiMsgIndex >= 0 && chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI) {
chatMsgList.value[aiMsgIndex].msg = connected ? "" : "发送消息失败,请重试";
chatMsgList.value[aiMsgIndex].isLoading = connected;
}
if (connected) {
// 连接成功后重新发送消息
sendChat(message, isInstruct);
} else {
console.error("WebSocket重新初始化失败");
}
}, 1000);
return;
}
const messageType = isInstruct ? 1 : 0;
const messageContent = isInstruct ? commonType : message;
// 生成 messageId 并保存到当前会话变量stopRequest 可能使用)
currentSessionMessageId = IdUtils.generateMessageId();
// 插入AI消息并在 pendingMap 中记录
const aiMsg = {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.AI,
msg: "加载中",
isLoading: true,
msgContent: {
type: MessageType.TEXT,
url: "",
},
messageId: currentSessionMessageId,
};
chatMsgList.value.push(aiMsg);
// 添加AI消息后滚动到底部
setTimeoutScrollToBottom();
const aiMsgIndex = chatMsgList.value.length - 1;
// 记录 pendingMap
pendingMap.set(currentSessionMessageId, aiMsgIndex);
// 启动超时回退
const timeoutId = setTimeout(() => {
const idx = pendingMap.get(currentSessionMessageId);
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();
}
}, MESSAGE_TIMEOUT);
pendingTimeouts.set(currentSessionMessageId, timeoutId);
// 发送消息,支持重连尝试
const success = await sendWebSocketMessage(messageType, messageContent, { messageId: currentSessionMessageId, tryReconnect: true });
if (!success) {
const idx = pendingMap.get(currentSessionMessageId);
if (idx != null && chatMsgList.value[idx]) {
chatMsgList.value[idx].msg = "发送消息失败,请重试";
chatMsgList.value[idx].isLoading = false;
}
// 清理 pending
if (pendingTimeouts.has(currentSessionMessageId)) {
clearTimeout(pendingTimeouts.get(currentSessionMessageId));
pendingTimeouts.delete(currentSessionMessageId);
}
pendingMap.delete(currentSessionMessageId);
isSessionActive.value = false;
}
};
// 停止请求函数
const stopRequest = async () => {
console.log("停止请求");
// 发送中断消息给服务器 (messageType=2),带上当前 messageId
try {
await sendWebSocketMessage(2, "stop_request", { messageId: currentSessionMessageId, silent: true });
} catch (e) {
console.warn("stopRequest send failed:", e);
}
// 直接将AI消息状态设为停止优先使用 pendingMap 映射)
let aiMsgIndex = -1;
if (currentSessionMessageId && pendingMap.has(currentSessionMessageId)) {
aiMsgIndex = pendingMap.get(currentSessionMessageId);
} else {
aiMsgIndex = chatMsgList.value.length - 1;
}
if (chatMsgList.value[aiMsgIndex] &&
chatMsgList.value[aiMsgIndex].msgType === MessageRole.AI) {
chatMsgList.value[aiMsgIndex].isLoading = false;
if (chatMsgList.value[aiMsgIndex].msg &&
chatMsgList.value[aiMsgIndex].msg.trim() &&
!chatMsgList.value[aiMsgIndex].msg.startsWith("加载中")) {
// 保留已显示内容
} else {
chatMsgList.value[aiMsgIndex].msg = "请求已停止";
}
}
// 清理 pending
if (currentSessionMessageId) {
if (pendingTimeouts.has(currentSessionMessageId)) {
clearTimeout(pendingTimeouts.get(currentSessionMessageId));
pendingTimeouts.delete(currentSessionMessageId);
}
pendingMap.delete(currentSessionMessageId);
}
// 重置会话状态
isSessionActive.value = false;
setTimeoutScrollToBottom();
};
// 组件销毁时清理资源
onUnmounted(() => {
uni.$off(NOTICE_EVENT_LOGIN_SUCCESS);
uni.$off(SCROLL_TO_BOTTOM);
uni.$off(SEND_MESSAGE_CONTENT_TEXT);
uni.$off(SEND_MESSAGE_COMMAND_TYPE);
uni.$off(NOTICE_EVENT_LOGOUT);
resetConfig();
});
const resetConfig = () => {
// 清理WebSocket连接
if (webSocketManager) {
webSocketManager.destroy();
webSocketManager = null;
}
// 重置消息状态
resetMessageState();
isSessionActive.value = false;
// 清理 pendingMap / pendingTimeouts
try {
for (const t of pendingTimeouts.values()) {
clearTimeout(t);
}
} catch (e) {
// ignore
}
pendingTimeouts.clear();
pendingMap.clear();
// 清理定时器
if (holdKeyboardTimer.value) {
clearTimeout(holdKeyboardTimer.value);
holdKeyboardTimer.value = null;
}
};
</script>
<style lang="scss" scoped></style>