546 lines
12 KiB
Vue
546 lines
12 KiB
Vue
<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> |