网页接入弹窗客服功能的完整实现(Vue3 + WebSocket预备方案)
以上就是一个基础弹窗客服到websocket实时通信的完整实现步骤。后续可以进一步扩展的功能包括: 客服坐席状态显示, 消息已读回执, 文件传输功能, 客服评价系统。
一、基础弹窗客服功能实现
1. 组件结构设计
首先创建一个ChatSupport.vue组件,包含以下核心部分:
<template> <div class="chat-container"> <!-- 悬浮按钮 --> <div class="chat-button" @click="toggleChat"> <img src="客服图标.png" alt="客服"/> </div> <!-- 聊天窗口 --> <div class="chat-window"> <!-- 消息区域 --> <div class="messages-wrapper"> <div class="messages"> <!-- 消息列表 --> </div> </div> <!-- 输入区域 --> <div class="input-area"> <!-- 输入框和工具 --> </div> </div> </div> </template>
2. 核心功能实现步骤
2.1 状态管理
import { ref, computed, watch, nextTick } from 'vue'; export default { setup() { // 基本状态 const isOpen = ref(false); const inputMessage = ref(''); const messages = ref([]); // 其他状态... return { isOpen, inputMessage, messages, // 其他方法... }; } };
2.2 消息发送与接收
const sendMessage = () => { if (!inputMessage.value.trim()) return; // 添加用户消息 addMessage({ sender: 'user', type: 'text', content: inputMessage.value.trim(), time: new Date() }); inputMessage.value = ''; // 模拟AI回复 simulateAIResponse(); }; const addMessage = (message) => { messages.value.push(message); scrollToBottom(); }; const scrollToBottom = () => { nextTick(() => { const container = document.querySelector('.messages-wrapper'); if (container) { container.scrollTop = container.scrollHeight; } }); };
2.3 模拟AI回复
const aiResponses = [ "您好,请问有什么可以帮您?", // 更多预设回复... ]; const simulateAIResponse = () => { setTimeout(() => { const response = aiResponses[Math.floor(Math.random() * aiResponses.length)]; addMessage({ sender: 'ai', type: 'text', content: response, time: new Date() }); }, 1000); };
3. 样式设计要点
.chat-container { position: fixed; bottom: 20px; right: 20px; z-index: 1000; } .chat-window { width: 350px; height: 500px; background: white; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.2); display: flex; flex-direction: column; } .messages-wrapper { flex: 1; overflow-y: auto; padding: 15px; } .input-area { padding: 10px; border-top: 1px solid #eee; }
二、历史记录图片表情包功能实现
1. 历史记录管理
const chatSessions = ref([{ startTime: new Date(), messages: [] }]); const currentSessionIndex = ref(0); const currentSession = computed(() => { return chatSessions.value[currentSessionIndex.value]; }); const loadSession = (index) => { currentSessionIndex.value = index; };
2. 图片发送与预览
const handleImageUpload = (e) => { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = (event) => { addMessage({ sender: 'user', type: 'image', content: event.target.result, time: new Date() }); }; reader.readAsDataURL(file); };
3. 表情包支持
const emojis = ['😀', '😂', '😍', /*...*/]; const showEmojiPicker = ref(false); const selectEmoji = (emoji) => { inputMessage.value += emoji; showEmojiPicker.value = false; };
三、接入WebSocket方案
1. WebSocket基础集成
const socket = ref(null); const connectWebSocket = () => { socket.value = new WebSocket('wss://your-websocket-endpoint'); socket.value.onopen = () => { console.log('WebSocket连接已建立'); }; socket.value.onmessage = (event) => { const message = JSON.parse(event.data); addMessage({ sender: 'ai', type: 'text', content: message.content, time: new Date() }); }; socket.value.onclose = () => { console.log('WebSocket连接已关闭'); }; }; // 组件挂载时连接 onMounted(() => { connectWebSocket(); }); // 组件卸载时断开连接 onUnmounted(() => { if (socket.value) { socket.value.close(); } });
2. 消息发送改造
const sendMessage = () => { if (!inputMessage.value.trim()) return; const message = { sender: 'user', type: 'text', content: inputMessage.value.trim(), time: new Date() }; // 添加到本地消息列表 addMessage(message); // 通过WebSocket发送 if (socket.value && socket.value.readyState === WebSocket.OPEN) { socket.value.send(JSON.stringify({ type: 'text', content: message.content })); } inputMessage.value = ''; };
3. 处理不同类型的消息
socket.value.onmessage = (event) => { const data = JSON.parse(event.data); switch(data.type) { case 'text': addMessage({ sender: 'ai', type: 'text', content: data.content, time: new Date() }); break; case 'image': addMessage({ sender: 'ai', type: 'image', content: data.url, time: new Date() }); break; case 'system': // 处理系统通知 break; default: console.warn('未知消息类型:', data.type); } };
4. 心跳检测与重连机制
const heartbeatInterval = ref(null); const reconnectAttempts = ref(0); const maxReconnectAttempts = 5; const setupHeartbeat = () => { heartbeatInterval.value = setInterval(() => { if (socket.value.readyState === WebSocket.OPEN) { socket.value.send(JSON.stringify({ type: 'heartbeat' })); } }, 30000); }; const reconnect = () => { if (reconnectAttempts.value < maxReconnectAttempts) { reconnectAttempts.value++; setTimeout(connectWebSocket, 1000 * reconnectAttempts.value); } }; // 在connectWebSocket中添加 socket.value.onclose = () => { console.log('连接断开,尝试重连...'); reconnect(); };
四、完整实现的最佳实践
1. 错误处理与状态管理
const connectionStatus = ref('disconnected'); // 'connecting', 'connected', 'error' const connectWebSocket = () => { connectionStatus.value = 'connecting'; socket.value = new WebSocket('wss://your-endpoint'); socket.value.onerror = (error) => { connectionStatus.value = 'error'; console.error('WebSocket错误:', error); }; // ...其他事件处理 };
2. 消息队列处理
const messageQueue = ref([]); const isProcessingQueue = ref(false); const processQueue = () => { if (isProcessingQueue.value || messageQueue.value.length === 0) return; isProcessingQueue.value = true; const message = messageQueue.value.shift(); if (socket.value.readyState === WebSocket.OPEN) { socket.value.send(JSON.stringify(message)); isProcessingQueue.value = false; processQueue(); // 处理下一条 } else { // 等待连接恢复 messageQueue.value.unshift(message); isProcessingQueue.value = false; } }; // 修改sendMessage const sendMessage = () => { // ... messageQueue.value.push({ type: 'text', content: message.content }); processQueue(); // ... };
3. 性能优化建议
-
虚拟滚动:对于大量消息实现虚拟滚动
<VirtualScroll :items="messages" :item-height="60"> <template v-slot="{ item }"> <!-- 消息渲染 --> </template> </VirtualScroll> -
消息分页加载:
const loadMoreMessages = () => { if (isLoading.value) return; isLoading.value = true; // 加载更多消息... }; -
WebSocket二进制传输:对于图片等大数据量内容
socket.value.binaryType = 'arraybuffer';
五、部署与安全考虑
- WSS协议:生产环境务必使用
wss://安全连接 - 认证机制:
socket.value.onopen = () => { socket.value.send(JSON.stringify({ type: 'auth', token: '用户令牌' })); }; - 消息加密:敏感内容应加密传输
- 限流控制:服务器端实现消息频率限制
结语
以上就是一个基础弹窗客服到websocket实时通信的完整实现步骤。后续可以进一步扩展的功能包括: 客服坐席状态显示, 消息已读回执, 文件传输功能, 客服评价系统
总体代码
<template> <div class="chat-container" :class="{ 'chat-open': isOpen }"> <div class="chat-button" @click="toggleChat"> <span v-if="unreadCount > 0" class="unread-badge">{{ unreadCount }}</span> <!-- <i class="icon-chat"></i> --> <img src="http://192.168.80.32:8888/客服.png" width="60%" alt="" /> </div> <div class="chat-window"> <div class="chat-header"> <h3>在线客服</h3> <div class="header-actions"> <button @click="toggleHistory" class="history-btn"> {{ showHistory ? "返回当前对话" : "历史记录" }} </button> <button @click="toggleChat" class="close-btn">×</button> </div> </div> <div class="chat-body"> <!-- 历史记录面板 --> <div v-if="showHistory" class="history-panel"> <div class="history-list"> <div v-for="(session, index) in currentSession.messages" :key="index" class="history-item" :class="{ active: currentSessionIndex === index }" > <div class="history-time"> {{ formatTime(session.time) }} </div> <img style="width: 100%" v-if="session.type === 'image'" :src="session.content" /> <span v-else-if="session.type === 'emoji'" class="emoji-message"> {{ session.content }} </span> <div class="history-preview"> {{ session.content || "无消息" }} </div> </div> </div> </div> <!-- 当前聊天面板 --> <div v-else class="message-panel"> <div class="messages-wrapper" ref="messagesContainer"> <div class="messages"> <div v-for="(message, index) in currentSession.messages" :key="index" class="message" :class="{ user: message.sender === 'user', ai: message.sender === 'ai', image: message.type === 'image', emoji: message.type === 'emoji', }" > <div class="message-content"> <img v-if="message.type === 'image'" :src="message.content" @click="previewImage(message.content)" /> <span v-else-if="message.type === 'emoji'" class="emoji-message" > {{ message.content }} </span> <span v-else>{{ message.content }}</span> </div> <div class="message-time">{{ formatTime(message.time) }}</div> </div> </div> </div> <div class="input-area"> <div class="toolbar"> <button @click="toggleEmojiPicker" class="tool-btn"> <i class="icon-emoji">😊</i> </button> <button @click="triggerFileInput" class="tool-btn"> <i class="icon-image">📷</i> </button> <input type="file" ref="fileInput" @change="handleImageUpload" accept="image/*" style="display: none" /> </div> <div v-if="showEmojiPicker" class="emoji-picker"> <span v-for="emoji in emojis" :key="emoji" @click="selectEmoji(emoji)" >{{ emoji }}</span > </div> <textarea v-model="inputMessage" @keyup.enter="sendMessage" placeholder="输入消息..." ref="textInput" ></textarea> <button @click="sendMessage" class="send-btn">发送</button> </div> </div> </div> </div> <!-- 图片预览模态框 --> <div v-if="previewImageUrl" class="image-preview-modal" @click="previewImageUrl = null" > <img :src="previewImageUrl" /> </div> </div> </template> <script> import { ref, computed, watch, nextTick, onMounted } from "vue"; export default { name: "ChatSupport", setup() { // 状态管理 const isOpen = ref(false); const inputMessage = ref(""); const showEmojiPicker = ref(false); const showHistory = ref(false); const previewImageUrl = ref(null); const unreadCount = ref(0); const fileInput = ref(null); const textInput = ref(null); const messagesContainer = ref(null); const currentSessionIndex = ref(0); // 模拟AI回复的简单逻辑 const aiResponses = [ "您好,请问有什么可以帮您?", "我明白了,正在为您处理...", "这个问题我们需要进一步核实", "感谢您的耐心等待", "请问您能提供更多细节吗?", "我们已经记录您的问题", "建议您尝试刷新页面", "这个问题可能需要技术支持介入", "我理解您的不便,非常抱歉", "我们会尽快解决这个问题", ]; // 表情包列表 const emojis = ["😀", "😂", "😍", "🤔", "😎", "👍", "❤️", "🙏", "🎉", "🔥"]; // 聊天会话数据 const chatSessions = ref([ { startTime: new Date(), messages: [ { sender: "ai", type: "text", content: "您好,请问有什么可以帮您?", time: new Date(), }, ], }, ]); // 计算当前会话 const currentSession = computed(() => { return chatSessions.value[currentSessionIndex.value]; }); // 切换聊天窗口 const toggleChat = () => { isOpen.value = !isOpen.value; if (isOpen.value) { unreadCount.value = 0; nextTick(() => { scrollToBottom(); textInput.value?.focus(); }); } }; // 计算属性获取所有消息 const allMessages = computed(() => { return chatSessions.value.flatMap((session) => session.messages); }); // 发送消息 const sendMessage = () => { if (!inputMessage.value.trim()) return; // 添加用户消息 addMessage({ sender: "user", type: "text", content: inputMessage.value.trim(), time: new Date(), }); inputMessage.value = ""; // // 模拟AI回复 setTimeout(async () => { // const randomResponse = // aiResponses[Math.floor(Math.random() * aiResponses.length)]; const randomResponse = await connectToAIService( inputMessage.value.trim() ); addMessage({ sender: "ai", type: "text", content: randomResponse, time: new Date(), }); }, 500 + Math.random() * 200); // 1-3秒延迟 }; // 添加消息到当前会话 const addMessage = (message) => { currentSession.value.messages.push(message); nextTick(() => { scrollToBottom(); }); // 如果窗口关闭且有新AI消息,增加未读计数 if (!isOpen.value && message.sender === "ai") { unreadCount.value++; } }; // 滚动到底部 const scrollToBottom = () => { nextTick(() => { console.log("我被执行了", messagesContainer.value); if (messagesContainer.value) { messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight; } }); }; // 切换表情选择器 const toggleEmojiPicker = () => { showEmojiPicker.value = !showEmojiPicker.value; }; // 选择表情 const selectEmoji = (emoji) => { inputMessage.value += emoji; showEmojiPicker.value = false; textInput.value?.focus(); }; // 触发文件选择 const triggerFileInput = () => { fileInput.value?.click(); }; // 处理图片上传 const handleImageUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { addMessage({ sender: "user", type: "image", content: event.target.result, time: new Date(), }); // 模拟AI回复图片 setTimeout(() => { addMessage({ sender: "ai", type: "text", content: "收到您发送的图片,正在为您处理...", time: new Date(), }); }, 1500); }; reader.readAsDataURL(file); e.target.value = ""; // 重置input }; // 预览图片 const previewImage = (url) => { previewImageUrl.value = url; }; // 切换历史记录面板 const toggleHistory = () => { showHistory.value = !showHistory.value; }; // 加载历史会话 const loadSession = (index) => { currentSessionIndex.value = index; showHistory.value = false; nextTick(() => { scrollToBottom(); }); }; // 创建新会话 const createNewSession = () => { chatSessions.value.push({ startTime: new Date(), messages: [ { sender: "ai", type: "text", content: "您好,请问有什么可以帮您?", time: new Date(), }, ], }); currentSessionIndex.value = chatSessions.value.length - 1; }; // 格式化时间 const formatTime = (date) => { return new Date(date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); }; // 格式化日期 const formatDate = (date) => { return new Date(date).toLocaleDateString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; // 模拟连接AI服务 const connectToAIService = async (message) => { // 这里可以替换为实际的AI API调用 // 例如: const response = await fetch('your-ai-api-endpoint', {...}); return new Promise((resolve) => { setTimeout(() => { const randomResponse = aiResponses[Math.floor(Math.random() * aiResponses.length)]; resolve(randomResponse); }, 1000); }); }; // 组件挂载时初始化 onMounted(() => { scrollToBottom(); // 可以在这里添加初始化逻辑,比如从本地存储加载历史会话 const savedSessions = localStorage.getItem("chatSessions"); if (savedSessions) { chatSessions.value = JSON.parse(savedSessions); } }); // 监视会话变化保存到本地存储 watch( chatSessions, (newVal) => { localStorage.setItem("chatSessions", JSON.stringify(newVal)); }, { deep: true } ); watch( () => currentSession.value.messages.length, () => { scrollToBottom(); }, { deep: true } ); return { scrollToBottom, isOpen, inputMessage, showEmojiPicker, showHistory, previewImageUrl, unreadCount, fileInput, textInput, messagesContainer, currentSessionIndex, emojis, chatSessions, currentSession, toggleChat, sendMessage, toggleEmojiPicker, selectEmoji, triggerFileInput, handleImageUpload, previewImage, toggleHistory, loadSession, createNewSession, formatTime, formatDate, }; }, }; </script> <style scoped> .chat-container { position: fixed; bottom: 20px; right: 20px; z-index: 1000; font-family: "Arial", sans-serif; } .chat-button { width: 60px; height: 60px; background-color: #1890ff; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); position: relative; transition: all 0.3s ease; } .chat-button:hover { background-color: #40a9ff; transform: scale(1.05); } .unread-badge { position: absolute; top: -5px; right: -5px; background-color: #f5222d; color: white; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 12px; } .chat-window { width: 350px; height: 500px; background-color: white; border-radius: 10px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); overflow: hidden; display: none; flex-direction: column; transform: translateY(20px); opacity: 0; transition: all 0.3s ease; } .chat-open .chat-window { display: flex; transform: translateY(0); opacity: 1; } .chat-header { padding: 15px; background-color: #1890ff; color: white; display: flex; justify-content: space-between; align-items: center; } .chat-header h3 { margin: 0; font-size: 16px; } .header-actions { display: flex; gap: 10px; } .history-btn, .close-btn { background: none; border: none; color: white; cursor: pointer; font-size: 12px; padding: 5px; } .close-btn { font-size: 20px; line-height: 1; } .chat-body { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .history-panel { flex: 1; overflow-y: auto; padding: 10px; } .history-list { display: flex; flex-direction: column; gap: 10px; } .history-item { padding: 10px; border-radius: 5px; background-color: #f5f5f5; cursor: pointer; transition: background-color 0.2s; } .history-item:hover { background-color: #e6f7ff; } .history-item.active { background-color: #1890ff; color: white; } .history-time { font-size: 12px; margin-bottom: 5px; } .history-preview { font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .message-panel { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .messages { flex: 1; padding: 15px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; } .message { max-width: 80%; padding: 10px 15px; border-radius: 18px; position: relative; word-wrap: break-word; } .message.user { align-self: flex-end; background-color: #1890ff; color: white; border-bottom-right-radius: 5px; } .message.ai { align-self: flex-start; background-color: #f5f5f5; color: #333; border-bottom-left-radius: 5px; } .message.image { padding: 5px; background-color: transparent; } .message.emoji { font-size: 24px; background-color: transparent; padding: 5px; } .message-content img { max-width: 100%; max-height: 200px; border-radius: 10px; cursor: zoom-in; } .message-time { font-size: 10px; color: #999; margin-top: 5px; text-align: right; } .user .message-time { color: rgba(255, 255, 255, 0.7); } .input-area { padding: 10px; border-top: 1px solid #eee; position: relative; } .toolbar { display: flex; gap: 10px; margin-bottom: 5px; } .tool-btn { background: none; border: none; cursor: pointer; font-size: 18px; padding: 5px; color: #666; } .emoji-picker { position: absolute; bottom: 60px; left: 10px; background: white; border: 1px solid #eee; border-radius: 10px; padding: 10px; display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; max-height: 150px; overflow-y: auto; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); z-index: 10; } .emoji-picker span { cursor: pointer; font-size: 20px; padding: 5px; } .emoji-picker span:hover { transform: scale(1.2); } textarea { width: 100%; border: 1px solid #ddd; border-radius: 20px; padding: 10px 15px; resize: none; min-height: 40px; max-height: 100px; outline: none; font-family: inherit; } .send-btn { position: absolute; right: 20px; bottom: 20px; background-color: #1890ff; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; cursor: pointer; } .send-btn:hover { background-color: #40a9ff; } .image-preview-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 2000; } .image-preview-modal img { max-width: 90%; max-height: 90%; } .messages-wrapper { flex: 1; overflow-y: auto; padding: 15px; } .messages { display: flex; flex-direction: column; gap: 15px; min-height: min-content; } .input-area { padding: 10px; border-top: 1px solid #eee; position: relative; flex-shrink: 0; /* 防止输入区域被压缩 */ background: white; /* 确保输入区域覆盖在消息上 */ z-index: 1; /* 确保输入区域在上层 */ } </style>
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)