Files
YGChatCS/pages/chat/ChatMainList.vue
2025-08-10 21:27:41 +08:00

720 lines
20 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="chat-container" @touchend="handleTouchEnd">
<!-- 顶部的背景 -->
<ChatTopBgImg class="chat-container-bg"></ChatTopBgImg>
<view class="chat-content">
<!-- 顶部自定义导航栏 -->
<view
class="nav-bar-container"
:style="{
paddingTop: statusBarHeight + 'px',
}"
>
<ChatTopNavBar @openDrawer="openDrawer"></ChatTopNavBar>
</view>
<!-- 消息列表可滚动区域 -->
<scroll-view
:scroll-top="scrollTop"
scroll-y
:scroll-with-animation="true"
class="area-msg-list"
>
<!-- welcome栏 -->
<ChatTopWelcome
class="chat-container-top-bannar"
:initPageImages="mainPageDataModel.initPageImages"
:welcomeContent="mainPageDataModel.welcomeContent"
>
</ChatTopWelcome>
<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="message-item-ai"
:key="`ai-${item.msgId}-${item.msg ? item.msg.length : 0}`"
:text="item.msg || ''"
>
<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.createWorkOrderCard
"
/>
<DetailCardCompontent
v-else-if="
item.toolCall.componentName === ''
"
:toolCall="item.toolCall"
/>
</template>
<template #footer>
<!-- 这个是底部 -->
<AttachListComponent
v-if="item.question"
:question="item.question"
@replySent="handleReply"
/>
</template>
</ChatCardAI>
</template>
<template v-else-if="item.msgType === MessageRole.ME">
<ChatCardMine class="message-item-mine" :text="item.msg">
</ChatCardMine>
</template>
<template v-else>
<ChatCardOther class="message-item-other" :text="item.msg">
<ChatMoreTips
@replySent="handleReply"
:itemList="mainPageDataModel.guideWords"
/>
<ActivityListComponent
v-if="mainPageDataModel.activityList.length > 0"
:activityList="mainPageDataModel.activityList"
/>
<RecommendPostsComponent
v-if="mainPageDataModel.recommendTheme.length > 0"
:recommendThemeList="mainPageDataModel.recommendTheme"
/>
</ChatCardOther>
</template>
</view>
</scroll-view>
<!-- 输入框区域 -->
<view class="footer-area">
<ChatQuickAccess @replySent="handleReplyInstruct" />
<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>
</view>
</template>
<script setup >
import { onMounted, nextTick, onUnmounted, ref, defineEmits } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import {
SCROLL_TO_BOTTOM,
RECOMMEND_POSTS_TITLE,
SEND_COMMAND_TEXT,
} from "@/constant/constant";
import { WSS_URL } from "@/constant/base";
import { MessageRole, MessageType, CompName } from "../../model/ChatModel";
import ChatTopWelcome from "./ChatTopWelcome.vue";
import ChatTopBgImg from "./ChatTopBgImg.vue";
import ChatTopNavBar from "./ChatTopNavBar.vue";
import ChatCardAI from "./ChatCardAI.vue";
import ChatCardMine from "./ChatCardMine.vue";
import ChatCardOther from "./ChatCardOther.vue";
import ChatQuickAccess from "./ChatQuickAccess.vue";
import ChatMoreTips from "./ChatMoreTips.vue";
import ChatInputArea from "./ChatInputArea.vue";
import QuickBookingComponent from "../module/booking/QuickBookingComponent.vue";
import DiscoveryCardComponent from "../module/discovery/DiscoveryCardComponent.vue";
import ActivityListComponent from "../module/banner/ActivityListComponent.vue";
import RecommendPostsComponent from "../module/recommend/RecommendPostsComponent.vue";
import AttachListComponent from "../module/attach/AttachListComponent.vue";
import CreateServiceOrder from "@/components/CreateServiceOrder/index.vue";
import DetailCardCompontent from "../module/detail/DetailCardCompontent.vue";
import { mainPageData } from "@/request/api/MainPageDataApi";
import {
conversationMsgList,
recentConversation,
} from "@/request/api/ConversationApi";
import WebSocketManager from "@/utils/WebSocketManager";
import TypewriterManager from "@/utils/TypewriterManager";
import { IdUtils } from "@/utils";
/// 导航栏相关
const statusBarHeight = ref(20);
/// 输入框组件引用
const inputAreaRef = ref(null);
const timer = 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("");
/// 从个渠道获取如二维,没有的时候就返回首页的数据
const sceneId = ref("");
/// agentId 首页接口中获取
const agentId = ref("1");
/// 会话ID 历史数据接口中获取
const conversationId = ref("");
/// 首页的数据
const mainPageDataModel = ref({});
// 会话进行中标志
const isSessionActive = ref(false);
/// 指令
let commonType = "";
// WebSocket 相关
let webSocketManager = null;
// 打字机管理器
let typewriterManager = null;
// 当前会话的消息ID用于保持发送和终止的messageId一致
let currentSessionMessageId = null;
let loadingTimer = null;
// 打开抽屉
const emits = defineEmits(["openDrawer"]);
const openDrawer = () => emits("openDrawer");
const handleTouchEnd = () => {
clearTimeout(timer.value);
timer.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 scrollToBottom = () => {
nextTick(() => {
scrollTop.value = 99999;
// 强制触发滚动更新
setTimeout(() => {
scrollTop.value = scrollTop.value + 1;
}, 10);
});
};
// 延时滚动
const setTimeoutScrollToBottom = () => setTimeout(() => scrollToBottom(), 100);
// 发送普通消息
const handleReply = (text) => {
// 重置消息状态准备接收新的AI回复
resetMessageState();
sendMessage(text);
setTimeoutScrollToBottom();
};
// 是发送指令
const handleReplyInstruct = (item) => {
if (item.type === "MyOrder") {
// 订单
uni.navigateTo({
url: "/pages/order/list",
});
return;
}
commonType = item.type;
// 重置消息状态准备接收新的AI回复
resetMessageState();
sendMessage(item.title, true);
setTimeoutScrollToBottom();
};
// 输入区的发送消息事件
const sendMessageAction = (inputText) => {
console.log("输入消息:", inputText);
if (!inputText.trim()) return;
handleNoHideKeyboard();
currentSessionMessageId = IdUtils.generateMessageId();
// 重置消息状态准备接收新的AI回复
resetMessageState();
sendMessage(inputText);
// 发送消息后保持键盘状态
if (holdKeyboard.value && inputAreaRef.value) {
setTimeout(() => {
inputAreaRef.value.focusInput();
}, 100);
}
setTimeoutScrollToBottom();
};
onLoad(() => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20;
},
});
});
onMounted(async () => {
try {
getMainPageData();
await loadRecentConversation();
loadConversationMsgList();
addNoticeListener();
initTypewriterManager();
initWebSocket();
} catch (error) {
console.error("页面初始化错误:", error);
}
});
// 初始化WebSocket
const initWebSocket = () => {
// 清理旧实例
if (webSocketManager) {
webSocketManager.destroy();
}
// 使用配置的WebSocket服务器地址
const token = uni.getStorageSync("token");
const wsUrl = `${WSS_URL}?access_token=${token}`;
// 初始化WebSocket管理器
webSocketManager = new WebSocketManager({
wsUrl: wsUrl,
reconnectInterval: 3000, // 重连间隔
maxReconnectAttempts: 5, // 最大重连次数
heartbeatInterval: 30000, // 心跳间隔
// 连接成功回调
onOpen: (event) => {
// 重置会话状态
isSessionActive.value = true;
},
// 连接断开回调
onClose: (event) => {
console.error("WebSocket连接断开:", event);
// 停止当前会话
isSessionActive.value = false;
// 停止打字机效果
if (typewriterManager) {
typewriterManager.stopTypewriter();
}
},
// 错误回调
onError: (error) => {
isSessionActive.value = false;
console.error("WebSocket错误:", error);
},
// 消息回调
onMessage: (data) => {
handleWebSocketMessage(data);
},
// 获取会话ID回调 (用于心跳检测)
getConversationId: () => conversationId.value,
// 获取代理ID回调 (用于心跳检测)
getAgentId: () => agentId.value,
});
// 初始化连接
webSocketManager.connect().catch((error) => {
console.error("WebSocket连接失败:", error);
});
};
// 处理WebSocket消息
const handleWebSocketMessage = (data) => {
if(loadingTimer) {
clearInterval(loadingTimer);
loadingTimer = null;
}
const aiMsgIndex = chatMsgList.value.length - 1;
if (!chatMsgList.value[aiMsgIndex] || aiMsgIndex < 0) {
return;
}
//console.log("处理WebSocket消息:", data);
// 确保消息内容是字符串类型
if (data.content && typeof data.content !== "string") {
data.content = String(data.content);
}
// 处理content分片内容
if (data.content) {
// 使用打字机管理器添加内容
if (typewriterManager) {
typewriterManager.addContent(data.content);
}
// 确保新内容到达时页面保持在底部
setTimeoutScrollToBottom();
}
// 处理完成状态
if (data.finish) {
clearInterval(loadingTimer);
loadingTimer = null;
// 等待打字机完成后处理其他数据
const waitForTypingComplete = () => {
const status = typewriterManager
? typewriterManager.getStatus()
: { isTyping: false };
if (status.isTyping) {
setTimeout(waitForTypingComplete, 50);
return;
}
const msg = chatMsgList.value[aiMsgIndex].msg;
console.log('全量消息内容:', msg)
if (!msg || msg === '加载中.' || msg.startsWith('加载中')) {
chatMsgList.value[aiMsgIndex].msg = '未获取到内容,请重试';
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;
};
waitForTypingComplete();
}
};
// 初始化打字机管理器
const initTypewriterManager = () => {
if (typewriterManager) {
typewriterManager.destroy();
}
typewriterManager = new TypewriterManager({
typingSpeed: 50,
cursorText: '<text class="typing-cursor">|</text>',
});
// 设置回调函数
typewriterManager.setCallbacks({
// 每个字符打字时的回调
onCharacterTyped: (displayedContent) => {
// 立即滚动到底部,确保打字过程中始终可见
nextTick(() => {
scrollTop.value = 99999;
// 双重保险,确保滚动生效
setTimeout(() => {
scrollTop.value = 99999 + Math.random();
}, 20);
});
},
// 内容更新时的回调
onContentUpdate: (content) => {
const aiMsgIndex = chatMsgList.value.length - 1;
if (aiMsgIndex >= 0 && chatMsgList.value[aiMsgIndex]) {
chatMsgList.value[aiMsgIndex].msg = content;
}
},
// 打字完成时的回调
onTypingComplete: (finalContent) => {
const aiMsgIndex = chatMsgList.value.length - 1;
if (aiMsgIndex >= 0 && chatMsgList.value[aiMsgIndex]) {
chatMsgList.value[aiMsgIndex].msg = finalContent;
}
// 打字完成后最后一次滚动到底部
setTimeoutScrollToBottom();
},
});
};
// 重置消息状态
const resetMessageState = () => {
if (typewriterManager) {
typewriterManager.reset();
}
};
const addNoticeListener = () => {
uni.$on(SCROLL_TO_BOTTOM, () => {
setTimeout(() => {
scrollToBottom();
}, 200);
});
uni.$on(RECOMMEND_POSTS_TITLE, (value) => {
console.log("RECOMMEND_POSTS_TITLE:", value);
if (value && value.length > 0) {
handleReply(value);
}
});
uni.$on(SEND_COMMAND_TEXT, (value) => {
console.log("SEND_COMMAND_TEXT:", value);
if (value && value.length > 0) {
commonType = "Command.quickBooking";
sendMessage(value, true);
setTimeoutScrollToBottom();
}
});
};
// 获取最近一次的会话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 res = await mainPageData(sceneId.value);
if (res.code === 0) {
mainPageDataModel.value = res.data;
agentId.value = res.data.agentId;
initData();
setTimeoutScrollToBottom();
}
};
// 初始化数据 首次数据加载的时候
const initData = () => {
const msg = {
msgId: `msg_${0}`,
msgType: MessageRole.OTHER,
msg: "",
};
chatMsgList.value.push(msg);
};
// 发送消息的参数拼接
const sendMessage = (message, isInstruct = false) => {
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;
// 重置消息状态,为新消息做准备
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;
// 动态加载中动画
let dotCount = 1;
loadingTimer && clearInterval(loadingTimer);
loadingTimer = setInterval(() => {
dotCount = (dotCount % 3) + 1;
chatMsgList.value[aiMsgIndex].msg = "加载中" + ".".repeat(dotCount);
}, 400);
// 发送消息
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 });
// 停止打字机效果
if (typewriterManager) {
typewriterManager.stopTypewriter();
}
// 重置会话状态和消息状态
isSessionActive.value = false;
resetMessageState();
// 更新最后一条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.startsWith("加载中")
) {
chatMsgList.value[aiMsgIndex].msg = "请求已停止";
}
}
console.log("请求已停止,状态已重置");
setTimeoutScrollToBottom();
};
// 组件销毁时清理资源
onUnmounted(() => {
uni.$off(SCROLL_TO_BOTTOM);
uni.$off(RECOMMEND_POSTS_TITLE);
uni.$off(SEND_COMMAND_TEXT);
// 清理WebSocket连接
if (webSocketManager) {
webSocketManager.destroy();
webSocketManager = null;
}
// 清理打字机管理器
if (typewriterManager) {
typewriterManager.destroy();
typewriterManager = null;
}
// 重置消息状态
resetMessageState();
// 清理定时器
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
}
});
</script>
<style lang="scss" scoped>
@import "styles/ChatMainList.scss";
</style>