Files
lucky_shop/components/ai-chat-float/ai-chat-float.vue
2025-11-03 16:31:53 +08:00

546 lines
12 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>
<!-- 浮动AI客服按钮 -->
<view
v-if="showFloatButton"
class="ai-float-button"
:class="{ 'ai-float-button-mini': isChatOpen }"
:style="buttonStyle"
@click="toggleChat">
<!-- 按钮图标 -->
<view class="ai-float-icon">
<text class="iconfont" :class="isChatOpen ? 'icon-close' : 'icon-ai'"></text>
</view>
<!-- 未读消息小红点 -->
<view v-if="unreadCount > 0" class="ai-float-badge">
{{ unreadCount > 99 ? '99+' : unreadCount }}
</view>
<!-- 按钮文字 -->
<view v-if="!isChatOpen" class="ai-float-text">AI客服</view>
</view>
<!-- AI聊天弹窗 -->
<view
v-if="isChatOpen"
class="ai-chat-modal"
:class="{ 'ai-chat-modal-fullscreen': isFullscreen }">
<!-- 遮罩层 -->
<view class="ai-chat-mask" @click="closeChat"></view>
<!-- 聊天窗口 -->
<view class="ai-chat-window">
<!-- 聊天窗口头部 -->
<view class="ai-chat-header">
<view class="ai-chat-title">
<text class="iconfont icon-ai"></text>
<text>AI智能客服</text>
</view>
<view class="ai-chat-actions">
<!-- 全屏/最小化按钮 -->
<button class="action-btn" @click="toggleFullscreen">
<text class="iconfont" :class="isFullscreen ? 'icon-minimize' : 'icon-fullscreen'"></text>
</button>
<!-- 关闭按钮 -->
<button class="action-btn" @click="closeChat">
<text class="iconfont icon-close"></text>
</button>
</view>
</view>
<!-- 聊天内容区域 -->
<view class="ai-chat-content">
<ai-chat-message
ref="chatMessage"
:initial-messages="chatMessages"
:user-avatar="userAvatar"
:ai-avatar="aiAvatar"
:enable-streaming="true"
@message-sent="onMessageSent"
@ai-response="onAIResponse"
@history-loaded="onHistoryLoaded"
@file-preview="onFilePreview"
@audio-play="onAudioPlay"
@video-play="onVideoPlay"
@link-open="onLinkOpen"
@product-view="onProductView"
@action-click="onActionClick"
@input-change="onInputChange" />
</view>
</view>
</view>
</view>
</template>
<script>
import aiChatMessage from '../ai-chat-message/ai-chat-message.vue'
export default {
name: 'ai-chat-float',
components: {
aiChatMessage
},
props: {
// 浮动按钮位置
position: {
type: String,
default: 'bottom-right', // bottom-right, bottom-left, top-right, top-left
validator: (value) => {
return ['bottom-right', 'bottom-left', 'top-right', 'top-left'].includes(value)
}
},
// 按钮距离边缘的距离
offset: {
type: Object,
default: () => ({
bottom: '100rpx',
right: '40rpx',
top: '100rpx',
left: '40rpx'
})
},
// 用户头像
userAvatar: {
type: String,
default: '/static/images/default-avatar.png'
},
// AI头像
aiAvatar: {
type: String,
default: '/static/images/ai-avatar.png'
},
// 初始聊天消息
initialMessages: {
type: Array,
default: () => []
},
// 是否显示浮动按钮
showFloatButton: {
type: Boolean,
default: true
},
// 是否自动打开聊天窗口
autoOpen: {
type: Boolean,
default: false
}
},
data() {
return {
isChatOpen: false, // 聊天窗口是否打开
isFullscreen: false, // 是否全屏
unreadCount: 0, // 未读消息数量
chatMessages: [], // 聊天消息列表
messageId: 0 // 消息ID计数器
}
},
computed: {
// 按钮样式
buttonStyle() {
const style = {}
// 根据位置设置样式
if (this.position.includes('bottom')) {
style.bottom = this.offset.bottom
} else {
style.top = this.offset.top
}
if (this.position.includes('right')) {
style.right = this.offset.right
} else {
style.left = this.offset.left
}
return style
}
},
created() {
// 初始化聊天消息
this.chatMessages = [...this.initialMessages]
this.messageId = this.chatMessages.length
// 如果有初始消息,计算未读数量
this.updateUnreadCount()
// 如果设置了自动打开,则打开聊天窗口
if (this.autoOpen) {
this.openChat()
}
},
methods: {
// 切换聊天窗口
toggleChat() {
if (this.isChatOpen) {
this.closeChat()
} else {
this.openChat()
}
},
// 打开聊天窗口
openChat() {
this.isChatOpen = true
this.unreadCount = 0 // 打开后清空未读消息
this.$emit('chat-open')
},
// 关闭聊天窗口
closeChat() {
this.isChatOpen = false
this.isFullscreen = false
this.$emit('chat-close')
},
// 切换全屏模式
toggleFullscreen() {
this.isFullscreen = !this.isFullscreen
this.$emit('fullscreen-toggle', this.isFullscreen)
},
// 更新未读消息数量
updateUnreadCount() {
// 计算AI发送的未读消息数量
this.unreadCount = this.chatMessages.filter(msg =>
msg.role === 'ai' && !msg.read
).length
this.$emit('unread-update', this.unreadCount)
},
// 发送欢迎消息
sendWelcomeMessage() {
const welcomeMessage = {
id: ++this.messageId,
role: 'ai',
type: 'text',
content: '您好我是AI智能客服很高兴为您服务有什么可以帮助您的吗',
timestamp: Date.now(),
read: false
}
this.chatMessages.push(welcomeMessage)
this.updateUnreadCount()
},
// 消息发送事件
onMessageSent(message) {
this.$emit('message-sent', message)
},
// AI回复事件
onAIResponse(message) {
// 标记为未读
message.read = false
this.updateUnreadCount()
this.$emit('ai-response', message)
},
// 历史消息加载事件
onHistoryLoaded(messages) {
this.$emit('history-loaded', messages)
},
// 文件预览事件
onFilePreview(message) {
this.$emit('file-preview', message)
},
// 音频播放事件
onAudioPlay(message) {
this.$emit('audio-play', message)
},
// 视频播放事件
onVideoPlay(message) {
this.$emit('video-play', message)
},
// 链接打开事件
onLinkOpen(message) {
this.$emit('link-open', message)
},
// 商品查看事件
onProductView(message) {
this.$emit('product-view', message)
},
// 操作点击事件
onActionClick(data) {
this.$emit('action-click', data)
},
// 输入变化事件
onInputChange(value) {
this.$emit('input-change', value)
},
// 添加消息到聊天
addMessage(message) {
this.chatMessages.push({
...message,
id: ++this.messageId,
timestamp: message.timestamp || Date.now()
})
// 如果是AI消息增加未读计数
if (message.role === 'ai' && !message.read) {
this.updateUnreadCount()
}
},
// 清空聊天记录
clearMessages() {
this.chatMessages = []
this.messageId = 0
this.unreadCount = 0
this.$emit('messages-clear')
},
// 标记所有消息为已读
markAllAsRead() {
this.chatMessages.forEach(msg => {
if (msg.role === 'ai') {
msg.read = true
}
})
this.updateUnreadCount()
this.$emit('messages-read')
}
}
}
</script>
<style lang="scss" scoped>
/* 浮动按钮样式 */
.ai-float-button {
position: fixed;
z-index: 9998;
display: flex;
align-items: center;
background: linear-gradient(135deg, #ff4544, #ff6b6b);
border-radius: 50rpx;
padding: 20rpx 30rpx;
box-shadow: 0 8rpx 30rpx rgba(255, 69, 68, 0.3);
transition: all 0.3s ease;
cursor: pointer;
&.ai-float-button-mini {
padding: 20rpx;
border-radius: 50%;
.ai-float-text {
display: none;
}
}
&:active {
transform: scale(0.95);
box-shadow: 0 4rpx 15rpx rgba(255, 69, 68, 0.5);
}
}
.ai-float-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15rpx;
.iconfont {
font-size: 40rpx;
color: white;
}
}
.ai-float-badge {
position: absolute;
top: -10rpx;
right: -10rpx;
background-color: #ff4544;
color: white;
border-radius: 30rpx;
padding: 4rpx 12rpx;
font-size: 20rpx;
font-weight: bold;
min-width: 30rpx;
text-align: center;
border: 2rpx solid white;
}
.ai-float-text {
color: white;
font-size: 28rpx;
font-weight: bold;
white-space: nowrap;
}
/* 聊天弹窗样式 */
.ai-chat-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
&.ai-chat-modal-fullscreen {
.ai-chat-window {
width: 100vw;
height: 100vh;
border-radius: 0;
max-width: none;
max-height: none;
}
}
}
.ai-chat-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
animation: fadeIn 0.3s ease;
}
.ai-chat-window {
position: absolute;
bottom: 150rpx;
right: 40rpx;
width: 600rpx;
height: 800rpx;
max-width: 90vw;
max-height: 80vh;
background-color: white;
border-radius: 20rpx;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideUp 0.3s ease;
/* 响应式调整 */
@media (max-width: 750rpx) {
width: 90vw;
height: 70vh;
bottom: 100rpx;
right: 5vw;
}
}
/* 聊天窗口头部 */
.ai-chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
background: linear-gradient(135deg, #ff4544, #ff6b6b);
color: white;
.ai-chat-title {
display: flex;
align-items: center;
font-size: 32rpx;
font-weight: bold;
.iconfont {
font-size: 36rpx;
margin-right: 15rpx;
}
}
.ai-chat-actions {
display: flex;
gap: 15rpx;
.action-btn {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
border: none;
.iconfont {
font-size: 28rpx;
color: white;
}
&:active {
background-color: rgba(255, 255, 255, 0.3);
}
}
}
}
/* 聊天内容区域 */
.ai-chat-content {
flex: 1;
overflow: hidden;
/* 确保聊天组件充满整个区域 */
::v-deep .ai-chat-container {
height: 100%;
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(100rpx) scale(0.9);
opacity: 0;
}
to {
transform: translateY(0) scale(1);
opacity: 1;
}
}
/* 响应式设计 */
@media (max-width: 750rpx) {
.ai-float-button {
padding: 25rpx;
&.ai-float-button-mini {
padding: 25rpx;
}
.ai-float-text {
font-size: 24rpx;
}
}
.ai-chat-window {
border-radius: 15rpx;
}
.ai-chat-header {
padding: 25rpx;
.ai-chat-title {
font-size: 28rpx;
}
}
}
</style>