Files
YGChatCS/pages/chat/ChatMainList.vue

511 lines
14 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" :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"/>
</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, computed } from 'vue'
import { ref } from 'vue'
import { defineEmits } from 'vue'
import { onLoad } from '@dcloudio/uni-app';
import { SCROLL_TO_BOTTOM, RECOMMEND_POSTS_TITLE, SEND_COMMAND_TEXT } from '@/constant/constant'
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 CommandWrapper from '@/components/CommandWrapper/index.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 { agentChatStream, stopAbortTask } from '@/request/api/AgentChatStream';
import { mainPageData } from '@/request/api/MainPageDataApi';
import { conversationMsgList, recentConversation } from '@/request/api/ConversationApi';
/// 导航栏相关
const statusBarHeight = ref(20);
/// 输入框组件引用
const inputAreaRef = ref(null);
const timer = ref(null)
/// focus时点击页面的时候不收起键盘
const holdKeyboard = ref(false)
/// 是否在键盘弹出,点击界面时关闭键盘
const holdKeyboardFlag = ref(true)
/// 键盘高度
const keyboardHeight = ref(0)
/// 是否显示键盘
const isKeyboardShow = ref(false)
///(控制滚动位置)
const scrollTop = ref(99999);
/// 会话列表
const chatMsgList = ref([])
/// 输入口的输入消息
const inputMessage = ref('')
/// 加载中
let currentAIMsgIndex = 0
/// 从个渠道获取如二维,没有的时候就返回首页的数据
const sceneId = ref('')
/// agentId 首页接口中获取
const agentId = ref('1')
/// 会话ID 历史数据接口中获取
const conversationId = ref('')
/// 首页的数据
const mainPageDataModel = ref({})
// 会话进行中标志
const isSessionActive = ref(false);
/// 指令
let commonType = ''
// 打开抽屉
const emits = defineEmits(['openDrawer'])
const openDrawer = () => {
emits('openDrawer')
console.log('=============打开抽屉')
}
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 = (height) => {
keyboardHeight.value = height
isKeyboardShow.value = true
holdKeyboard.value = true
// 键盘弹起时调整聊天内容的底部边距并滚动到底部
setTimeout(() => {
scrollToBottom()
}, 150)
}
// 键盘收起事件
const handleKeyboardHide = () => {
keyboardHeight.value = 0
isKeyboardShow.value = false
holdKeyboard.value = false
}
/// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
nextTick(() => {
scrollTop.value += 9999;
});
});
}
/// 延迟在滚到底
const setTimeoutScrollToBottom = () => {
setTimeout(() => {
scrollToBottom()
}, 100)
}
/// 发送普通消息
const handleReply = (text) => {
sendMessage(text)
setTimeoutScrollToBottom()
};
/// 是发送指令
const handleReplyInstruct = (item) => {
if(item.type === 'MyOrder') {
/// 订单
uni.navigateTo({
url: '/pages/order/list'
})
return
}
commonType = item.type
sendMessage(item.title, true)
setTimeoutScrollToBottom()
}
/// 输入区的发送消息事件
const sendMessageAction = (inputText) => {
console.log("输入消息:", inputText)
if (!inputText.trim()) return;
handleNoHideKeyboard()
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() => {
getMainPageData()
await loadRecentConversation()
loadConversationMsgList()
addNoticeListener()
})
const addNoticeListener = () => {
uni.$on(SCROLL_TO_BOTTOM, (value) => {
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()
scrollToBottom()
}
}
/// 初始化数据 首次数据加载的时候
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))
}
/// 打字机效果实现的变量
let loadingTimer = null;
let typeWriterTimer = null;
let aiMsgBuffer = ''; // 全局缓冲区
let isTyping = false; // 是否正在打字
/// 发送获取AI聊天消息
const sendChat = (message, isInstruct = false) => {
const args = {
conversationId: conversationId.value,
agentId: agentId.value,
messageType: isInstruct ? 1 : 0,
messageContent: isInstruct ? commonType : message,
messageId: 'mid' + new Date().getTime()
}
// 插入AI消息
const aiMsg = {
msgId: `msg_${chatMsgList.value.length}`,
msgType: MessageRole.AI,
msg: '加载中.',
msgContent: {
type: MessageType.TEXT,
url: ''
}
}
chatMsgList.value.push(aiMsg)
const aiMsgIndex = chatMsgList.value.length - 1
currentAIMsgIndex = aiMsgIndex
// 动态加载中动画
let dotCount = 1;
loadingTimer && clearInterval(loadingTimer);
loadingTimer = setInterval(() => {
dotCount = dotCount % 3 + 1;
chatMsgList.value[aiMsgIndex].msg = '加载中' + '.'.repeat(dotCount);
}, 400);
aiMsgBuffer = '';
isTyping = false;
if (typeWriterTimer) {
clearTimeout(typeWriterTimer);
typeWriterTimer = null;
}
// 流式接收内容
const promise = agentChatStream(args, (chunk) => {
// console.log('分段内容:', chunk)
if (chunk && chunk.error) {
chatMsgList.value[aiMsgIndex].msg = '请求错误,请重试';
clearInterval(loadingTimer);
loadingTimer = null;
isTyping = false;
typeWriterTimer = null;
isSessionActive.value = false; // 出错也允许再次发送
console.error('流式错误:', chunk.message, chunk.detail);
return;
}
if (chunk && chunk.content) {
// 收到内容,停止动画
if (loadingTimer) {
clearInterval(loadingTimer);
loadingTimer = null;
}
// 把新内容追加到缓冲区
aiMsgBuffer += chunk.content;
// 启动打字机(只启动一次)
if (!isTyping) {
isTyping = true;
chatMsgList.value[aiMsgIndex].msg = '';
typeWriter();
}
}
if (chunk && chunk.finish) {
// 结尾处理:确保剩余内容全部输出
const finishInterval = setInterval(() => {
if (aiMsgBuffer.length === 0) {
clearInterval(finishInterval);
clearInterval(loadingTimer);
loadingTimer = null;
isTyping = false;
typeWriterTimer = null;
// 补全:如果消息内容还停留在'加载中.'或为空,则给出友好提示
const msg = chatMsgList.value[aiMsgIndex].msg;
console.log('msg:', msg)
if (!msg || msg === '加载中.' || msg.startsWith('加载中')) {
chatMsgList.value[aiMsgIndex].msg = '未获取到内容,请重试';
if(chunk.toolCall) {
chatMsgList.value[aiMsgIndex].msg = '';
}
}
// 如果有组件
if(chunk.toolCall) {
console.log('chunk.toolCall:', chunk.toolCall)
chatMsgList.value[aiMsgIndex].toolCall = chunk.toolCall
}
// 如果有问题,则设置问题
if(chunk.question && chunk.question.length > 0) {
chatMsgList.value[aiMsgIndex].question = chunk.question
}
isSessionActive.value = false;
scrollToBottom();
}
}, 50);
}
})
// 可选处理Promise完成/失败, 已经在回调中处理数据,此处无需再处理
promise.then(data => {
console.log('请求完成');
}).catch(err => {
isSessionActive.value = false; // 出错也允许再次发送
console.log('error:', err);
});
// 打字机函数
function typeWriter() {
if (aiMsgBuffer.length > 0) {
chatMsgList.value[aiMsgIndex].msg += aiMsgBuffer[0];
aiMsgBuffer = aiMsgBuffer.slice(1);
nextTick(() => {
scrollToBottom();
});
typeWriterTimer = setTimeout(typeWriter, 30);
} else {
// 等待新内容到来,不结束
typeWriterTimer = setTimeout(typeWriter, 30);
}
}
}
// 停止请求函数
const stopRequest = () => {
stopAbortTask()
// 重置状态
isSessionActive.value = false;
const msg = chatMsgList.value[currentAIMsgIndex].msg;
if (!msg || msg === '加载中.' || msg.startsWith('加载中')) {
chatMsgList.value[currentAIMsgIndex].msg = '已终止请求,请重试';
}
// 清除计时器
if (loadingTimer) {
clearInterval(loadingTimer);
loadingTimer = null;
}
if (typeWriterTimer) {
clearTimeout(typeWriterTimer);
typeWriterTimer = null;
}
setTimeoutScrollToBottom()
}
</script>
<style lang="scss" scoped>
@import "styles/ChatMainList.scss";
</style>