m2pool_web_frontend/mining-pool/src/components/ChatWidget.vue

893 lines
24 KiB
Vue

<template>
<div class="chat-widget">
<!-- 聊天图标 -->
<div
class="chat-icon"
@click="toggleChat"
:class="{ active: isChatOpen }"
aria-label="打开客服聊天"
tabindex="0"
@keydown.enter="toggleChat"
@keydown.space="toggleChat"
>
<i class="el-icon-chat-dot-round"></i>
<span v-if="unreadMessages > 0" class="unread-badge">{{
unreadMessages
}}</span>
</div>
<!-- 聊天对话框 -->
<transition name="chat-slide">
<div v-show="isChatOpen" class="chat-dialog">
<div class="chat-header">
<div class="chat-title">{{ $t("chat.title") || "在线客服" }}</div>
<div class="chat-actions">
<i class="el-icon-minus" @click="minimizeChat"></i>
<i class="el-icon-close" @click="closeChat"></i>
</div>
</div>
<div class="chat-body" ref="chatBody">
<!-- 连接状态提示 -->
<div v-if="connectionStatus === 'connecting'" class="chat-status connecting">
<i class="el-icon-loading"></i>
<p>正在连接客服系统...</p>
</div>
<div v-else-if="connectionStatus === 'error'" class="chat-status error">
<i class="el-icon-warning"></i>
<p>连接失败,请稍后重试</p>
<button @click="connectWebSocket" class="retry-button">重试连接</button>
</div>
<!-- 消息列表 -->
<template v-else>
<div v-if="messages.length === 0" class="chat-empty">
{{ $t("chat.welcome") || "欢迎使用在线客服,请问有什么可以帮您?" }}
</div>
<div
v-for="(msg, index) in messages"
:key="index"
class="chat-message"
:class="{
'chat-message-user': msg.type === 'user',
'chat-message-system': msg.type === 'system',
}"
>
<div class="message-avatar">
<i v-if="msg.type === 'system'" class="el-icon-service"></i>
<i v-else class="el-icon-user"></i>
</div>
<div class="message-content">
<!-- 文本消息 -->
<div v-if="!msg.isImage" class="message-text">{{ msg.text }}</div>
<!-- 图片消息 -->
<div v-else class="message-image">
<img :src="msg.imageUrl" @click="previewImage(msg.imageUrl)" alt="聊天图片" />
</div>
<div class="message-time">{{ formatTime(msg.time) }}</div>
</div>
</div>
</template>
</div>
<div class="chat-footer">
<div class="chat-toolbar">
<label for="imageUpload" class="image-upload-label" :class="{ 'disabled': connectionStatus !== 'connected' }">
<i class="el-icon-picture-outline"></i>
</label>
<input
type="file"
id="imageUpload"
ref="imageUpload"
accept="image/*"
@change="handleImageUpload"
style="display: none;"
:disabled="connectionStatus !== 'connected'"
/>
</div>
<input
type="text"
class="chat-input"
v-model="inputMessage"
@keyup.enter="sendMessage"
:placeholder="$t('chat.inputPlaceholder') || '请输入您的问题...'"
:disabled="connectionStatus !== 'connected'"
/>
<button
class="chat-send"
@click="sendMessage"
:disabled="connectionStatus !== 'connected' || !inputMessage.trim()"
>
{{ $t("chat.send") || "发送" }}
</button>
</div>
<!-- 图片预览 -->
<div v-if="showImagePreview" class="image-preview-overlay" @click="closeImagePreview">
<div class="image-preview-container">
<img :src="previewImageUrl" class="preview-image" />
<i class="el-icon-close preview-close" @click="closeImagePreview"></i>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
import { Client } from '@stomp/stompjs';
import { getUserid } from '../api/customerService';
export default {
name: "ChatWidget",
data() {
return {
isChatOpen: false,
inputMessage: "",
messages: [],
unreadMessages: 0,
// 图片预览相关
showImagePreview: false,
previewImageUrl: '',
// WebSocket 相关
stompClient: null,
connectionStatus: 'disconnected', // disconnected, connecting, connected, error
userType: 0, // 0 游客 1 登录用户 2 客服
userEmail: '', // 用户标识
// 自动回复配置
autoResponses: {
hello: "您好,有什么可以帮助您的?",
你好: "您好,有什么可以帮助您的?",
hi: "您好,有什么可以帮助您的?",
挖矿: "您可以查看我们的挖矿教程,或者直接创建矿工账户开始挖矿。",
算力: "您可以在首页查看当前的矿池算力和您的个人算力。",
收益: "收益根据您的算力贡献按比例分配,详情可以查看收益计算器。",
帮助: "您可以查看我们的帮助文档,或者提交工单咨询具体问题。",
},
};
},
mounted() {
document.addEventListener("click", this.handleClickOutside);
},
methods: {
async fetchUserid(){
const res = await getUserid();
if(res &&res.code == 200){
this.roomId = res.data;
console.log(res,"及附加覅");
}
},
// 初始化 WebSocket 连接
initWebSocket() {
this.determineUserType();
this.connectWebSocket();
},
// 确定用户类型和邮箱
determineUserType() {
try {
const token = JSON.parse(localStorage.getItem('token') || '{}');
const userInfo = JSON.parse(localStorage.getItem('jurisdiction') || '{}');
const email = JSON.parse(localStorage.getItem('userEmail') || '{}');
if (token) {
if (userInfo.roleKey === 'customer_service') {
// 客服用户
this.userType = 2;
} else {
// 登录用户
this.userType = 1;
}
this.userEmail =email;
}else{//游客
this.userType = 0;
this.userEmail = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
} catch (error) {
console.error('获取用户信息失败:', error);
// 出错时默认为游客
this.userType = 0;
this.userEmail = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
},
// 连接 WebSocket
connectWebSocket() {
this.connectionStatus = 'connecting';
try {
const wsUrl = `${process.env.VUE_APP_BASE_API}chat/ws`;
// 创建 STOMP 客户端
this.stompClient = new Client({
brokerURL: wsUrl,
connectHeaders: {
'email': this.userEmail,
'type': this.userType
},
debug: function(str) {
console.log('STOMP: ' + str);
},
reconnectDelay: 5000,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000
});
// 连接成功回调
this.stompClient.onConnect = (frame) => {
console.log('连接成功: ' + frame);
this.connectionStatus = 'connected';
// 订阅个人消息频道
this.stompClient.subscribe(`${process.env.VUE_APP_BASE_API}user/queue/${this.userEmail}`, this.onMessageReceived);
// 根据用户类型显示不同的欢迎消息
let welcomeMessage = '';
switch(this.userType) {
case 0:
welcomeMessage = '您当前以游客身份访问,请问有什么可以帮您?';
break;
case 1:
welcomeMessage = '欢迎回来,请问有什么可以帮您?';
break;
case 2:
welcomeMessage = '您已以客服身份登录系统';
break;
}
this.addSystemMessage(welcomeMessage);
};
// 连接错误回调
this.stompClient.onStompError = (frame) => {
console.error('连接错误: ' + frame.headers.message);
this.connectionStatus = 'error';
this.addSystemMessage('连接客服系统失败,请稍后重试。');
};
// 启动连接
this.stompClient.activate();
} catch (error) {
console.error('初始化 WebSocket 失败:', error);
this.connectionStatus = 'error';
}
},
// 断开 WebSocket 连接
disconnectWebSocket() {
if (this.stompClient && this.stompClient.connected) {
this.stompClient.deactivate();
this.connectionStatus = 'disconnected';
}
},
// 接收消息处理
onMessageReceived(message) {
console.log('收到消息:', message.body);
try {
const data = JSON.parse(message.body);
// 添加客服消息
this.messages.push({
type: 'system',
text: data.content,
isImage: data.type === 'image',
imageUrl: data.type === 'image' ? data.content : null,
time: new Date(),
});
// 如果聊天窗口没有打开,显示未读消息数
if (!this.isChatOpen) {
this.unreadMessages++;
}
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
} catch (error) {
console.error('解析消息失败:', error);
}
},
// 切换聊天窗口
toggleChat() {
this.isChatOpen = !this.isChatOpen;
if (this.isChatOpen) {
this.unreadMessages = 0;
// 如果未连接,则连接 WebSocket
if (this.connectionStatus === 'disconnected') {
this.initWebSocket();
}
this.$nextTick(() => {
this.scrollToBottom();
});
}
},
minimizeChat() {
this.isChatOpen = false;
},
closeChat() {
this.isChatOpen = false;
this.messages = [];
this.disconnectWebSocket();
},
// 发送消息
sendMessage() {
if (!this.inputMessage.trim() || this.connectionStatus !== 'connected') return;
const messageText = this.inputMessage.trim();
// 添加用户消息到界面
this.messages.push({
type:0,// 0 文本 1图片
text: messageText,
isImage: false,
time: new Date(),
email:"",// 接收者邮箱?
receiveUserType:2,// 接受用户类型0 游客 1 登录用户 2 客服人员
sendUserType:this.userType,// 发送者类型0 游客 1 登录用户 2 客服人员
roomId:this.roomId,// 聊天室ID
});
// 通过 WebSocket 发送消息
if (this.stompClient && this.stompClient.connected) {
this.stompClient.publish({
destination: '/send/message',
body: JSON.stringify({
content: messageText,
type: 'text'
})
});
} else {
// 如果连接失败,使用自动回复
this.handleAutoResponse(messageText);
}
// 清空输入框
this.inputMessage = "";
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
},
// 添加系统消息
addSystemMessage(text) {
this.messages.push({
type: "system",
text: text,
isImage: false,
time: new Date(),
});
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
},
// 自动回复 (仅在无法连接服务器时使用)
handleAutoResponse(message) {
setTimeout(() => {
let response = "抱歉,我暂时无法回答这个问题。请联系真人客服或提交工单。";
// 检查是否匹配自动回复关键词
for (const [keyword, reply] of Object.entries(this.autoResponses)) {
if (message.toLowerCase().includes(keyword.toLowerCase())) {
response = reply;
break;
}
}
// 添加系统回复
this.messages.push({
type: "system",
text: response,
isImage: false,
time: new Date(),
});
if (!this.isChatOpen) {
this.unreadMessages++;
}
this.$nextTick(() => {
this.scrollToBottom();
});
}, 1000);
},
scrollToBottom() {
if (this.$refs.chatBody) {
this.$refs.chatBody.scrollTop = this.$refs.chatBody.scrollHeight;
}
},
formatTime(date) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
},
handleClickOutside(event) {
if (this.isChatOpen) {
const chatElement = this.$el.querySelector('.chat-dialog');
const chatIcon = this.$el.querySelector('.chat-icon');
this.fetchUserid();
if (chatElement && !chatElement.contains(event.target) && !chatIcon.contains(event.target)) {
this.isChatOpen = false;
}
}
},
// 处理图片上传
handleImageUpload(event) {
if (this.connectionStatus !== 'connected') return;
const file = event.target.files[0];
if (!file) return;
// 检查是否为图片
if (!file.type.startsWith('image/')) {
this.$message({
message: this.$t('chat.onlyImages') || '只能上传图片文件!',
type: 'warning'
});
return;
}
// 检查文件大小 (限制为5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
this.$message({
message: this.$t('chat.imageTooLarge') || '图片大小不能超过5MB!',
type: 'warning'
});
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const imageUrl = e.target.result;
// 添加用户图片消息到界面
this.messages.push({
type:1,// 0 文本 1图片
text: "",
isImage: true,
imageUrl: imageUrl,
time: new Date(),
email:"",// 接收者邮箱?
receiveUserType:2,// 接受用户类型0 游客 1 登录用户 2 客服人员
sendUserType:this.userType,// 发送者类型0 游客 1 登录用户 2 客服人员
roomId:this.roomId,// 聊天室ID
});
// 通过 WebSocket 发送图片消息
if (this.stompClient && this.stompClient.connected) {
this.stompClient.publish({
destination: '/send/message',
body: JSON.stringify({
content: imageUrl,
type: 'image'
})
});
}
this.$nextTick(() => {
this.scrollToBottom();
});
};
reader.readAsDataURL(file);
this.$refs.imageUpload.value = '';
},
// 预览图片
previewImage(imageUrl) {
this.previewImageUrl = imageUrl;
this.showImagePreview = true;
},
// 关闭图片预览
closeImagePreview() {
this.showImagePreview = false;
this.previewImageUrl = '';
}
},
beforeDestroy() {
this.disconnectWebSocket();
document.removeEventListener("click", this.handleClickOutside);
}
};
</script>
<style scoped lang="scss">
.chat-widget {
position: fixed;
bottom: 40px;
right: 60px;
z-index: 1000;
font-family: Arial, sans-serif;
}
.chat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #AC85E0;
color: white;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
position: relative;
i {
font-size: 28px;
}
&:hover {
transform: scale(1.05);
background-color: #6E3EDB;
}
&.active {
background-color: #6E3EDB;
}
}
.unread-badge {
position: absolute;
top: -5px;
right: -5px;
background-color: #e74c3c;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
}
.chat-dialog {
position: absolute;
bottom: 80px;
right: 0;
width: 350px;
height: 450px;
background-color: white;
border-radius: 10px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background-color: #AC85E0;
color: white;
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-title {
font-weight: bold;
font-size: 16px;
}
.chat-actions {
display: flex;
gap: 15px;
i {
cursor: pointer;
font-size: 16px;
&:hover {
opacity: 0.8;
}
}
}
.chat-body {
flex: 1;
overflow-y: auto;
padding: 15px;
background-color: #f8f9fa;
}
.chat-status {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
i {
font-size: 32px;
margin-bottom: 16px;
}
p {
margin: 8px 0;
color: #666;
}
&.connecting i {
color: #AC85E0;
}
&.error {
i {
color: #e74c3c;
}
p {
color: #e74c3c;
}
}
.retry-button {
margin-top: 16px;
padding: 8px 16px;
background-color: #AC85E0;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
&:hover {
background-color: #6E3EDB;
}
}
}
.chat-empty {
color: #777;
text-align: center;
margin-top: 30px;
}
.chat-message {
display: flex;
margin-bottom: 15px;
&.chat-message-user {
flex-direction: row-reverse;
.message-content {
background-color: #AC85E0;
color: white;
border-radius: 18px 18px 0 18px;
}
.message-time {
text-align: right;
color: rgba(255, 255, 255, 0.7);
}
}
&.chat-message-system {
.message-content {
background-color: white;
border-radius: 18px 18px 18px 0;
}
}
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background-color: #e0e0e0;
margin: 0 10px;
i {
font-size: 18px;
color: #555;
}
}
.message-content {
max-width: 70%;
padding: 10px 15px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.message-text {
line-height: 1.4;
font-size: 14px;
word-break: break-word;
}
.message-image {
img {
max-width: 200px;
max-height: 200px;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s;
&:hover {
transform: scale(1.03);
}
}
}
.message-time {
font-size: 11px;
color: #999;
margin-top: 4px;
}
.chat-footer {
padding: 10px;
display: flex;
border-top: 1px solid #e0e0e0;
align-items: center;
}
.chat-toolbar {
margin-right: 8px;
}
.image-upload-label {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
color: #666;
&:hover:not(.disabled) {
color: #AC85E0;
}
&.disabled {
opacity: 0.5;
cursor: not-allowed;
}
i {
font-size: 20px;
}
}
.chat-input {
flex: 1;
border: 1px solid #ddd;
border-radius: 20px;
padding: 8px 15px;
outline: none;
&:focus:not(:disabled) {
border-color: #AC85E0;
}
&:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
}
.chat-send {
background-color: #AC85E0;
color: white;
border: none;
border-radius: 20px;
padding: 8px 15px;
margin-left: 10px;
cursor: pointer;
font-weight: bold;
&:hover:not(:disabled) {
background-color: #6E3EDB;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
// 图片预览
.image-preview-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
z-index: 1100;
display: flex;
justify-content: center;
align-items: center;
}
.image-preview-container {
position: relative;
max-width: 90%;
max-height: 90%;
}
.preview-image {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
}
.preview-close {
position: absolute;
top: -40px;
right: 0;
color: white;
font-size: 24px;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.5);
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
&:hover {
background-color: rgba(0, 0, 0, 0.8);
}
}
// 动画效果
.chat-slide-enter-active, .chat-slide-leave-active {
transition: all 0.3s ease;
}
.chat-slide-enter, .chat-slide-leave-to {
transform: translateY(20px);
opacity: 0;
}
// 移动端适配
@media (max-width: 768px) {
.chat-widget {
bottom: 20px;
right: 20px;
}
.chat-dialog {
width: 300px;
height: 400px;
bottom: 70px;
}
.message-image img {
max-width: 150px;
max-height: 150px;
}
}
</style>