From 452c2b50c627ce5ef1153222b1afe21e2111b6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A6=85=E9=A5=BC?= <2815246336@qq.com> Date: Tue, 19 Aug 2025 11:47:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=95=B0=E5=AD=97=E4=BA=BA?= =?UTF-8?q?=E5=A4=B4=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layout/components/Aichat/ChatPopup.vue | 364 ++++++++++++++++++++- 1 file changed, 353 insertions(+), 11 deletions(-) diff --git a/src/layout/components/Aichat/ChatPopup.vue b/src/layout/components/Aichat/ChatPopup.vue index 1857fb2..1dcb9ba 100644 --- a/src/layout/components/Aichat/ChatPopup.vue +++ b/src/layout/components/Aichat/ChatPopup.vue @@ -2,7 +2,26 @@
@@ -185,6 +204,20 @@ export default { // 请求监控 activeRequests: new Map(), // 存储活跃请求的信息 + + // 数字人相关状态 + isAISpeaking: false, // AI是否在说话 + isAIThinking: false, // AI是否在思考 + isBlinking: false, // 是否在眨眼 + blinkTimer: null, // 眨眼定时器 + speakingTimer: null, // 说话动画定时器 + + // 眼睛跟踪鼠标 + mouseX: 0, // 鼠标X坐标 + mouseY: 0, // 鼠标Y坐标 + eyeOffsetX: 0, // 眼睛X偏移 + eyeOffsetY: 0, // 眼睛Y偏移 + isMouseTracking: true, // 是否启用鼠标跟踪 requestMonitorInterval: null, maxRequestDuration: 60000, // 最大请求持续时间60秒 @@ -260,6 +293,12 @@ export default { // 启动请求监控 this.startRequestMonitoring() + // 启动数字人自动眨眼 + this.startAutoBlinking() + + // 启动鼠标跟踪 + this.enableMouseTracking() + // 确保DOM完全渲染后再初始化聊天 this.$nextTick(() => { setTimeout(async () => { @@ -299,6 +338,11 @@ export default { this.contentUpdateTimer = null } + // 清理数字人相关定时器和事件监听器 + this.stopAutoBlinking() + this.stopAIAnimation() + this.disableMouseTracking() + // 取消正在进行的请求 if (this.currentCancel) { this.currentCancel() @@ -718,10 +762,6 @@ export default { this.inputMessage = '' this.sending = true - // 定义超时变量 - let streamTimeout = null - let noDataTimeout = null - // 添加到请求队列 const requestId = Date.now() this.requestQueue.push(requestId) @@ -734,9 +774,6 @@ export default { messageId: 'user-' + Date.now() } this.messages.push(userMsg) - - // 添加用户消息后立即滚动到底部 - this.scrollToBottom(false, true) // 立即滚动,强制执行 // 添加AI消息占位符 const aiMsg = { @@ -750,7 +787,10 @@ export default { } this.messages.push(aiMsg) - this.scrollToBottom(true, true) // 添加消息后使用平滑滚动,强制执行 + // 启动AI思考动画 + this.startAIThinking() + + this.scrollToBottom(true) // 添加消息后使用平滑滚动 try { // 创建流式聊天 @@ -773,7 +813,7 @@ export default { }) // 添加额外的超时保护 - streamTimeout = setTimeout(() => { + const streamTimeout = setTimeout(() => { if (cancel) { cancel('流式响应超时') } @@ -788,6 +828,7 @@ export default { const { reader, decoder } = response let buffer = '' let lastUpdateTime = Date.now() + let noDataTimeout = null while (true) { // 设置无数据超时检测 @@ -830,6 +871,12 @@ export default { if (data.event === 'message' && data.answer) { const currentContent = aiMsg.content const newContent = (currentContent === '正在思考...' ? '' : currentContent) + data.answer + + // 如果是第一次收到回复,启动说话动画 + if (currentContent === '正在思考...') { + this.startAISpeaking() + } + // 使用节流更新方法,减少频繁的DOM更新和滚动 this.throttledContentUpdate(aiMsg, newContent) } @@ -855,6 +902,8 @@ export default { aiMsg.streamCompleted = true // 重置内容长度计数器 this.lastContentLength = 0 + // 停止AI动画 + this.stopAIAnimation() // 流式响应结束后强制滚动到底部 this.$nextTick(() => { this.scrollToBottom(true, true) // 使用平滑滚动并强制执行 @@ -913,7 +962,7 @@ export default { // 从监控系统中移除请求 this.activeRequests.delete(requestId) - this.scrollToBottom(true, true) // 发送完成后使用平滑滚动,强制执行 + this.scrollToBottom(true) // 发送完成后使用平滑滚动 } }, @@ -1103,6 +1152,139 @@ export default { } }, + /** + * 数字人控制方法 + */ + // 开始AI思考动画 + startAIThinking() { + this.isAIThinking = true + this.isAISpeaking = false + // 思考时启用鼠标跟踪 + this.enableMouseTracking() + }, + + // 开始AI说话动画 + startAISpeaking() { + this.isAIThinking = false + this.isAISpeaking = true + + // 说话时禁用鼠标跟踪,眼睛面向前方 + this.disableMouseTracking() + + // 清除之前的说话定时器 + if (this.speakingTimer) { + clearTimeout(this.speakingTimer) + } + }, + + // 停止AI动画 + stopAIAnimation() { + this.isAIThinking = false + this.isAISpeaking = false + + // 停止动画后恢复鼠标跟踪 + this.enableMouseTracking() + + if (this.speakingTimer) { + clearTimeout(this.speakingTimer) + this.speakingTimer = null + } + }, + + // 眨眼动画 + triggerBlink() { + if (this.isBlinking) return + + this.isBlinking = true + setTimeout(() => { + this.isBlinking = false + }, 150) + }, + + // 启动自动眨眼 + startAutoBlinking() { + if (this.blinkTimer) { + clearInterval(this.blinkTimer) + } + + this.blinkTimer = setInterval(() => { + if (!this.isDestroyed) { + this.triggerBlink() + } + }, 3000 + Math.random() * 2000) // 3-5秒随机眨眼 + }, + + // 停止自动眨眼 + stopAutoBlinking() { + if (this.blinkTimer) { + clearInterval(this.blinkTimer) + this.blinkTimer = null + } + }, + + // 鼠标移动事件处理 + handleMouseMove(event) { + if (!this.isMouseTracking || this.isDestroyed) return + + this.mouseX = event.clientX + this.mouseY = event.clientY + this.updateEyePosition() + }, + + // 更新眼睛位置 + updateEyePosition() { + if (!this.isMouseTracking) return + + // 获取数字人头像的位置 + const avatarElement = this.$el.querySelector('.digital-avatar') + if (!avatarElement) return + + const rect = avatarElement.getBoundingClientRect() + const avatarCenterX = rect.left + rect.width / 2 + const avatarCenterY = rect.top + rect.height / 2 + + // 计算鼠标相对于头像中心的位置 + const deltaX = this.mouseX - avatarCenterX + const deltaY = this.mouseY - avatarCenterY + + // 计算距离和角度 + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + const maxDistance = 100 // 最大跟踪距离 + const maxOffset = 1.5 // 眼睛最大偏移量 + + // 限制跟踪范围 + const trackingFactor = Math.min(distance / maxDistance, 1) + + // 计算眼睛偏移 + this.eyeOffsetX = (deltaX / distance) * maxOffset * trackingFactor + this.eyeOffsetY = (deltaY / distance) * maxOffset * trackingFactor + + // 如果距离太近,眼睛回到中心 + if (distance < 50) { + this.eyeOffsetX = 0 + this.eyeOffsetY = 0 + } + }, + + // 眼睛回到中心位置(说话时使用) + resetEyePosition() { + this.eyeOffsetX = 0 + this.eyeOffsetY = 0 + }, + + // 启用鼠标跟踪 + enableMouseTracking() { + this.isMouseTracking = true + document.addEventListener('mousemove', this.handleMouseMove) + }, + + // 禁用鼠标跟踪 + disableMouseTracking() { + this.isMouseTracking = false + document.removeEventListener('mousemove', this.handleMouseMove) + this.resetEyePosition() + }, + /** * 处理点赞 */ @@ -1546,11 +1728,171 @@ export default { } } +/* 导航栏左侧容器 */ +.nav-left { + display: flex; + align-items: center; + gap: 12px; +} + .nav-title { font-size: 16px; font-weight: 600; } +/* 数字人头像容器 */ +.digital-avatar { + position: relative; + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + transition: all 0.3s ease; + cursor: pointer; +} + +.digital-avatar:hover { + transform: scale(1.05); +} + +.digital-avatar.thinking { + animation: thinking-pulse 2s infinite; +} + +.digital-avatar.speaking { + animation: speaking-glow 0.5s infinite alternate; +} + +/* 头像发光效果 */ +.avatar-glow { + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border-radius: 50%; + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + opacity: 0; + z-index: -1; + transition: opacity 0.3s ease; +} + +.digital-avatar.speaking .avatar-glow { + opacity: 0.6; + animation: glow-pulse 1s infinite; +} + +/* 头像面部 */ +.avatar-face { + position: relative; + width: 32px; + height: 32px; + background: #fff; + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +/* 眼睛容器 */ +.avatar-eyes { + display: flex; + gap: 6px; + margin-bottom: 4px; +} + +/* 眼睛 */ +.eye { + width: 8px; + height: 8px; + background: #fff; + border: 1px solid #ddd; + border-radius: 50%; + transition: all 0.15s ease; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.eye.blink { + height: 2px; + background: #666; + border: none; +} + +.eye.blink .pupil { + display: none; +} + +/* 瞳孔 */ +.pupil { + width: 4px; + height: 4px; + background: #333; + border-radius: 50%; + transition: transform 0.1s ease; +} + +/* 嘴巴 */ +.avatar-mouth { + width: 8px; + height: 3px; + background: #333; + border-radius: 0 0 8px 8px; + transition: all 0.2s ease; +} + +.avatar-mouth.talking { + animation: mouth-talk 0.3s infinite alternate; +} + +/* 动画定义 */ +@keyframes thinking-pulse { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(79, 172, 254, 0.4); + } + 50% { + box-shadow: 0 0 0 8px rgba(79, 172, 254, 0); + } +} + +@keyframes speaking-glow { + 0% { + box-shadow: 0 0 10px rgba(79, 172, 254, 0.5); + } + 100% { + box-shadow: 0 0 20px rgba(79, 172, 254, 0.8); + } +} + +@keyframes glow-pulse { + 0%, 100% { + transform: scale(1); + opacity: 0.6; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } +} + +@keyframes mouth-talk { + 0% { + height: 3px; + width: 8px; + } + 100% { + height: 6px; + width: 10px; + } +} + .nav-close { font-size: 24px; cursor: pointer;