加入数字人头像

This commit is contained in:
2025-08-19 11:47:01 +08:00
parent 63a39f22a7
commit 452c2b50c6

View File

@@ -2,7 +2,26 @@
<div class="chat-popup">
<!-- 导航栏 -->
<div class="navbar">
<div class="nav-title">智水AI辅导员</div>
<div class="nav-left">
<!-- 数字人头像 -->
<div class="digital-avatar" :class="{ speaking: isAISpeaking, thinking: isAIThinking }">
<div class="avatar-face">
<div class="avatar-eyes">
<div class="eye left-eye" :class="{ blink: isBlinking }"
:style="{ transform: `translate(${eyeOffsetX}px, ${eyeOffsetY}px)` }">
<div class="pupil"></div>
</div>
<div class="eye right-eye" :class="{ blink: isBlinking }"
:style="{ transform: `translate(${eyeOffsetX}px, ${eyeOffsetY}px)` }">
<div class="pupil"></div>
</div>
</div>
<div class="avatar-mouth" :class="{ talking: isAISpeaking }"></div>
</div>
<div class="avatar-glow"></div>
</div>
<div class="nav-title">智水AI辅导员</div>
</div>
<div class="nav-close" @click="closeChat">×</div>
</div>
@@ -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)
@@ -735,9 +775,6 @@ export default {
}
this.messages.push(userMsg)
// 添加用户消息后立即滚动到底部
this.scrollToBottom(false, true) // 立即滚动,强制执行
// 添加AI消息占位符
const aiMsg = {
sender: 'ai',
@@ -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;