893 lines
24 KiB
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>
|