Files
YGChatCS/src/pages/index/components/chat/ChatMainList/index.vue
2025-11-13 19:43:03 +08:00

689 lines
19 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 class="flex flex-justify-center" :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";
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;
/// WebSocket 连接状态
let webSocketConnectStatus = false;
// 当前会话的消息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 (!webSocketConnectStatus) {
initHandler();
}
});
uni.$on(NOTICE_EVENT_LOGOUT, () => {
resetConfig();
uni.showToast({
title: "退出登录成功",
});
});
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");
if (!appStore.hasToken) 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 = () => {
// 清理旧实例
if (webSocketManager) {
webSocketManager.destroy();
}
// 使用配置的WebSocket服务器地址
const token = uni.getStorageSync("token");
const wsUrl = `${appStore.serverConfig.wssUrl}?access_token=${token}`;
// 初始化WebSocket管理器
webSocketManager = new WebSocketManager({
wsUrl: wsUrl,
reconnectInterval: 3000, // 重连间隔
maxReconnectAttempts: 5, // 最大重连次数
heartbeatInterval: 30000, // 心跳间隔
// 连接成功回调
onOpen: (event) => {
// 重置会话状态
webSocketConnectStatus = true;
isSessionActive.value = true;
},
// 连接断开回调
onClose: (event) => {
console.error("WebSocket连接断开:", event);
webSocketConnectStatus = false;
// 停止当前会话
isSessionActive.value = false;
},
// 错误回调
onError: (error) => {
webSocketConnectStatus = false;
isSessionActive.value = false;
console.error("WebSocket错误:", error);
},
// 消息回调
onMessage: (data) => {
handleWebSocketMessage(data);
},
// 获取会话ID回调 (用于心跳检测)
getConversationId: () => conversationId.value,
// 获取代理ID回调 (用于心跳检测)
getAgentId: () => agentId.value,
});
// 初始化连接
webSocketManager
.connect()
.then(() => {
webSocketConnectStatus = true;
})
.catch((error) => {
console.error("WebSocket连接失败:", error);
});
};
// 处理WebSocket消息
const handleWebSocketMessage = (data) => {
const aiMsgIndex = chatMsgList.value.length - 1;
if (!chatMsgList.value[aiMsgIndex] || aiMsgIndex < 0) {
return;
}
// 确保消息内容是字符串类型
if (data.content && typeof data.content !== "string") {
data.content = String(data.content);
}
// 直接拼接内容到AI消息
if (data.content) {
if (chatMsgList.value[aiMsgIndex].isLoading) {
chatMsgList.value[aiMsgIndex].msg = "";
}
chatMsgList.value[aiMsgIndex].msg += data.content;
chatMsgList.value[aiMsgIndex].isLoading = false;
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;
}
// 重置会话状态
isSessionActive.value = false;
}
};
// 重置消息状态
const resetMessageState = () => {};
// 初始化数据 首次数据加载的时候
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();
if (!webSocketConnectStatus) {
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 = "";
sendChat(message, isInstruct);
console.log("发送的新消息:", JSON.stringify(newMsg));
};
// 通用WebSocket消息发送函数
const sendWebSocketMessage = (messageType, messageContent, options = {}) => {
const args = {
conversationId: conversationId.value,
agentId: agentId.value,
messageType: String(messageType), // 消息类型 0-对话 1-指令 2-中断停止 3-心跳检测
messageContent: messageContent,
messageId: currentSessionMessageId,
};
try {
webSocketManager.sendMessage(args);
console.log(`WebSocket消息已发送 [类型:${messageType}]:`, args);
return true;
} catch (error) {
console.error("发送WebSocket消息失败:", error);
isSessionActive.value = false;
return false;
}
};
// 发送获取AI聊天消息
const sendChat = (message, isInstruct = false) => {
if (!webSocketManager || !webSocketManager.isConnected()) {
console.error("WebSocket未连接");
isSessionActive.value = false;
return;
}
const messageType = isInstruct ? 1 : 0;
const messageContent = isInstruct ? commonType : message;
currentSessionMessageId = IdUtils.generateMessageId();
// 重置消息状态,为新消息做准备
resetMessageState();
// 插入AI消息
const aiMsg = {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.AI,
msg: "加载中",
isLoading: true,
msgContent: {
type: MessageType.TEXT,
url: "",
},
};
chatMsgList.value.push(aiMsg);
const aiMsgIndex = chatMsgList.value.length - 1;
// 发送消息
const success = sendWebSocketMessage(messageType, messageContent);
if (!success) {
chatMsgList.value[aiMsgIndex].msg = "发送消息失败,请重试";
chatMsgList.value[aiMsgIndex].isLoading = false;
isSessionActive.value = false;
resetMessageState();
}
};
// 停止请求函数
const stopRequest = () => {
console.log("停止请求");
// 发送中断消息给服务器 (messageType=2)
sendWebSocketMessage(2, "stop_request", { silent: true });
// 直接将AI消息状态设为停止
const 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 = "请求已停止";
}
}
// 重置会话状态
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;
webSocketConnectStatus = false;
}
// 重置消息状态
resetMessageState();
// 清理定时器
if (holdKeyboardTimer.value) {
clearTimeout(holdKeyboardTimer.value);
holdKeyboardTimer.value = null;
}
};
</script>
<style lang="scss" scoped></style>