This commit is contained in:
2025-12-23 15:57:11 +08:00
parent 33a16342d3
commit 68daf391af
23 changed files with 608 additions and 523 deletions

View File

@@ -308,6 +308,10 @@
background: #FFFFFF;
border: 1px solid #E5E5E5;
border-radius: 12px;
min-height: 40px;
box-sizing: border-box;
display: flex;
align-items: center;
}
.user-bubble .message-text {
@@ -460,3 +464,43 @@
font-size: 18px;
color: #4b87ff;
}
// 打字指示器动画
.typing-indicator {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
padding: 8px 4px;
}
.typing-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #999;
animation: typing-bounce 1.4s infinite ease-in-out both;
}
.typing-dot:nth-child(1) {
animation-delay: -0.32s;
}
.typing-dot:nth-child(2) {
animation-delay: -0.16s;
}
.typing-dot:nth-child(3) {
animation-delay: 0s;
}
@keyframes typing-bounce {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}

View File

@@ -65,7 +65,13 @@
<text class="avatar-text">AI</text>
</view>
<view class="message-bubble bot-bubble">
<text class="message-text">{{item.content}}</text>
<!-- 加载动画:内容为空时显示 -->
<view class="typing-indicator" v-if="!item.content && isTyping">
<view class="typing-dot"></view>
<view class="typing-dot"></view>
<view class="typing-dot"></view>
</view>
<text class="message-text" v-else>{{item.content}}</text>
</view>
</view>
<text class="message-time">{{item.time}}</text>
@@ -112,9 +118,9 @@
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import WorkcaseCreator from '@/components/WorkcaseCreator/WorkcaseCreator.uvue'
import { guestAPI, workcaseChatAPI } from '@/api'
import { guestAPI, aiChatAPI } from '@/api'
import type { TbWorkcaseDTO } from '@/types'
import { AGENT_ID } from '@/config'
// 前端消息展示类型
interface ChatMessageItem {
type: 'user' | 'bot'
@@ -122,7 +128,7 @@
time: string
actions?: string[] | null
}
const agentId = AGENT_ID
// 响应式数据
const messages = ref<ChatMessageItem[]>([])
const inputText = ref<string>('')
@@ -141,7 +147,7 @@
userId: ''
})
const isMockMode = ref(true) // 开发环境mock模式
const userType = ref(false)
// AI 对话相关
const chatId = ref<string>('') // 当前会话ID
const currentTaskId = ref<string>('') // 当前任务ID用于停止
@@ -161,9 +167,9 @@
// 开发环境使用mock数据
if (isMockMode.value) {
userInfo.value = {
wechatId: '17857100375',
username: '测试用户',
phone: '17857100375',
wechatId: '17857100377',
username: '访客用户',
phone: '17857100377',
userId: ''
}
await doIdentify()
@@ -173,12 +179,12 @@
// 切换mock用户开发调试用
function switchMockUser() {
uni.showActionSheet({
itemList: ['员工 (17857100375)', '访客 (17857100376)'],
itemList: ['员工 (17857100375)', '访客 (17857100377)'],
success: (res) => {
if (res.tapIndex === 0) {
userInfo.value = { wechatId: '17857100375', username: '员工用户', phone: '17857100375', userId: '' }
} else {
userInfo.value = { wechatId: '17857100376', username: '访客用户', phone: '17857100376', userId: '' }
userInfo.value = { wechatId: '17857100377', username: '访客用户', phone: '17857100377', userId: '' }
}
doIdentify()
}
@@ -198,10 +204,16 @@
const loginDomain = res.data
uni.setStorageSync('token', loginDomain.token || '')
uni.setStorageSync('userInfo', JSON.stringify(loginDomain.user))
uni.setStorageSync('loginDomain', JSON.stringify(loginDomain))
uni.setStorageSync('wechatId', userInfo.value.wechatId)
userInfo.value.userId = loginDomain.user?.userId || ''
console.log('identify成功:', loginDomain)
uni.showToast({ title: '登录成功', icon: 'success' })
if(loginDomain.user.status == 'guest') {
userType.value = false
} else {
userType.value = true
}
} else {
console.error('identify失败:', res.message)
}
@@ -275,9 +287,11 @@
try {
// 如果没有会话ID先创建会话
if (!chatId.value) {
const createRes = await workcaseChatAPI.createChat({
const createRes = await aiChatAPI.createChat({
title: '智能助手对话',
userId: userInfo.value.userId || userInfo.value.wechatId
userId: userInfo.value.userId || userInfo.value.wechatId,
agentId: agentId,
userType: userType.value
})
if (createRes.success && createRes.data) {
chatId.value = createRes.data.chatId || ''
@@ -288,22 +302,24 @@
}
// 准备流式对话
const prepareRes = await workcaseChatAPI.prepareChatMessageSession({
const prepareRes = await aiChatAPI.prepareChatMessageSession({
chatId: chatId.value,
message: query
query: query,
agentId: agentId,
userType: userType.value,
userId: userInfo.value.userId
})
if (!prepareRes.success || !prepareRes.data) {
throw new Error(prepareRes.message || '准备对话失败')
}
const sessionId = prepareRes.data
console.log('准备流式对话成功:', sessionId)
// 添加空的AI消息占位
const messageIndex = messages.value.length
addMessage('bot', '')
// 建立SSE连接
streamChat(sessionId, messageIndex)
startStreamChat(sessionId, messageIndex)
} catch (error : any) {
console.error('AI聊天失败:', error)
isTyping.value = false
@@ -312,66 +328,30 @@
}
// SSE 流式对话
function streamChat(sessionId : string, messageIndex : number) {
const url = `http://localhost:8180${workcaseChatAPI.getStreamUrl(sessionId)}`
console.log('建立SSE连接:', url)
function startStreamChat(sessionId : string, messageIndex : number) {
console.log('建立SSE连接, sessionId:', sessionId)
const requestTask = uni.request({
url: url,
method: 'GET',
header: { 'Accept': 'text/event-stream' },
enableChunked: true,
success: (res : any) => {
console.log('SSE请求完成:', res)
isTyping.value = false
},
fail: (err) => {
console.error('SSE请求失败:', err)
isTyping.value = false
messages.value[messageIndex].content = '抱歉,网络连接失败,请稍后重试。'
}
})
// 监听分块数据
requestTask.onChunkReceived((res : any) => {
try {
const decoder = new TextDecoder('utf-8')
const text = decoder.decode(new Uint8Array(res.data))
console.log('收到分块数据:', text)
const lines = text.split('\n')
for (const line of lines) {
if (line.startsWith('data:')) {
const dataStr = line.substring(5).trim()
if (dataStr && dataStr !== '[DONE]') {
try {
const data = JSON.parse(dataStr)
const event = data.event
if (event === 'message' || event === 'agent_message') {
if (data.answer) {
messages.value[messageIndex].content += data.answer
}
} else if (event === 'message_end') {
isTyping.value = false
if (data.task_id) {
currentTaskId.value = data.task_id
}
} else if (event === 'error') {
console.error('SSE错误:', data.message)
isTyping.value = false
messages.value[messageIndex].content = data.message || '抱歉,发生错误,请稍后重试。'
}
} catch (e) {
console.log('解析SSE数据失败:', dataStr)
}
}
}
aiChatAPI.streamChat(sessionId, {
onMessage: (data) => {
if (data.answer) {
messages.value[messageIndex].content += data.answer
nextTick(() => scrollToBottom())
}
} catch (e) {
console.error('处理分块数据失败:', e)
},
onEnd: (taskId) => {
isTyping.value = false
if (taskId) {
currentTaskId.value = taskId
}
},
onError: (error) => {
console.error('SSE错误:', error)
isTyping.value = false
messages.value[messageIndex].content = error
},
onComplete: () => {
isTyping.value = false
}
nextTick(() => scrollToBottom())
})
}
@@ -491,7 +471,11 @@
// 滚动到底部
function scrollToBottom() {
scrollTop.value = 999999
// 先重置再设置大值,确保值变化触发滚动
scrollTop.value = scrollTop.value + 1
nextTick(() => {
scrollTop.value = 999999
})
}
// 联系人工客服