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

4178 lines
134 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>
<div class="chat-widget">
<!-- 添加网络状态提示 -->
<div class="network-status" v-if="networkStatus === 'offline'">
<i class="el-icon-warning"></i>
<span>{{ $t("chat.networkError") || "网络连接已断开" }}</span>
</div>
<!-- 聊天图标 -->
<div
class="chat-icon"
@click="toggleChat"
:class="{ active: isChatOpen }"
:aria-label="$t('chat.openCustomerService') || '打开客服聊天'"
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="userType === 0" class="guest-notice">
<span class="guest-notice-content">
<i class="el-icon-info"></i>
<span
>{{ $t("chat.guestNotice") || "游客模式下聊天记录不会保存," }}
<a @click="handleLoginClick" class="login-link">{{
$t("chat.loginToSave") || "登录"
}}</a>
{{ $t("chat.guestNotice2") || "后即可保存" }}
</span>
</span>
</div>
<!-- 连接状态提示 -->
<div
v-if="connectionStatus === 'connecting'"
class="chat-status connecting"
>
<i class="el-icon-loading"></i>
<p>
{{ $t("chat.connectToCustomerService") || "正在连接客服系统..." }}
</p>
</div>
<div
v-else-if="connectionStatus === 'error'"
class="chat-status error"
>
<i class="el-icon-warning"></i>
<p>
{{
connectionError ||
$t("chat.connectionFailed") ||
"连接失败,请稍后重试"
}}
</p>
<div class="error-actions">
<button @click="handleRetryConnect" class="retry-button">
{{ $t("chat.tryConnectingAgain") || "重试连接" }}
</button>
<button
v-if="showRefreshButton"
@click="refreshPage"
class="refresh-button"
>
{{ $t("chat.refreshPage") || "刷新页面" }}
</button>
</div>
</div>
<!-- 消息列表 -->
<template v-else>
<!-- 历史消息加载提示 -->
<div
v-if="hasMoreHistory && messages.length > 0"
class="history-indicator"
:class="{ 'no-more': !hasMoreHistory }"
@click.stop="loadMoreHistory"
>
<i class="el-icon-arrow-up"></i>
<span>{{
isLoadingHistory
? $t("chat.loading") || "加载中..."
: hasMoreHistory
? $t("chat.loadMore") || "加载更多历史消息"
: $t("chat.noMoreHistory") || "没有更多历史消息了"
}}</span>
</div>
<!-- 没有消息时的欢迎提示 -->
<div
v-if="messages.length === 0 && userType !== 0"
class="chat-empty"
>
{{
$t("chat.welcome") || "欢迎使用在线客服,请问有什么可以帮您?"
}}
</div>
<!-- 消息项 -->
<div
v-for="(msg, index) in displayMessages"
:key="
msg.id
? `msg-${msg.id}`
: msg.isTimeDivider
? `divider-${index}-${msg.time}`
: `sys-${index}-${Date.now()}`
"
>
<!-- 时间分割条 -->
<div v-if="msg.isTimeDivider" class="chat-time-divider">
{{ formatTimeDivider(msg.time) }}
</div>
<!-- 系统提示消息,如加载中、无更多消息等 -->
<div
v-else-if="msg.isLoading || msg.isSystemHint"
class="system-hint"
>
<i v-if="msg.isLoading" class="el-icon-loading"></i>
<span>{{ msg.text }}</span>
</div>
<!-- 普通消息 -->
<div
v-else
class="chat-message"
:class="{
'chat-message-user': msg.type === 'user',
'chat-message-system': msg.type === 'system',
'chat-message-loading': msg.isLoading,
'chat-message-hint': msg.isSystemHint,
'chat-message-history': msg.isHistory,
}"
>
<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">
<!-- 时间显示在右上角(注释掉,只显示时间分割条) -->
<!-- <span class="message-time">{{ formatTime(msg.time) }}</span> -->
<div
v-if="!msg.isImage"
class="message-text"
v-html="formatMessageText(msg.text)"
></div>
<div v-else class="message-image">
<img
:src="msg.imageUrl"
@click="previewImage(msg.imageUrl)"
:alt="$t('chat.picture') || '聊天图片'"
@load="handleImageLoad(msg)"
/>
</div>
<!-- <div class="message-footer">
<span
v-if="msg.type === 'user'"
class="message-read-status"
>
{{
msg.isRead
? $t("chat.read") || "已读"
: $t("chat.unread") || "未读"
}}
</span>
</div> -->
</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>
<div
class="chat-input-wrapper"
style="display: flex; align-items: center"
>
<input
type="text"
class="chat-input"
v-model="inputMessage"
:maxlength="maxMessageLength"
@input="handleInputMessage"
@keydown.enter="handleEnterKey"
:placeholder="$t('chat.inputPlaceholder') || '请输入您的问题...'"
:disabled="connectionStatus !== 'connected'"
/>
<!-- <span class="input-counter">{{ maxMessageLength - inputMessage.length }}</span> -->
</div>
<!-- :disabled="connectionStatus !== 'connected' || !inputMessage.trim()" -->
<button class="chat-send" @click="sendMessage">
{{ $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, Stomp } from "@stomp/stompjs";
import {
getUserid,
getHistory,
getHistory7,
getReadMessage,
getFileUpdate,
} from "../api/customerService";
export default {
name: "ChatWidget",
data() {
return {
isChatOpen: false,
inputMessage: "",
messages: [],
unreadMessages: 0, // 仍保留但从localStorage读取
// 图片预览相关
showImagePreview: false,
previewImageUrl: "",
// WebSocket 相关
stompClient: null,
receivingEmail: "",
connectionStatus: "disconnected", // disconnected, connecting, connected, error
userType: 0, // 0 游客 1 登录用户 2 客服
userEmail: "", // 用户标识
// 自动回复配置
autoResponses: {
hello: "您好,有什么可以帮助您的?",
你好: "您好,有什么可以帮助您的?",
hi: "您好,有什么可以帮助您的?",
挖矿: "您可以查看我们的挖矿教程,或者直接创建矿工账户开始挖矿。",
算力: "您可以在首页查看当前的矿池算力和您的个人算力。",
收益: "收益根据您的算力贡献按比例分配,详情可以查看收益计算器。",
帮助: "您可以查看我们的帮助文档,或者提交工单咨询具体问题。",
},
isLoadingHistory: false, // 是否正在加载历史消息
hasMoreHistory: true, // 是否还有更多历史消息
roomId: "",
isWebSocketConnected: false,
heartbeatInterval: null,
lastHeartbeatTime: null,
heartbeatTimeout: 120000, // 增加到120秒2分钟没有心跳就认为连接断开
cachedMessages: {}, // 缓存各聊天室的消息
isMinimized: false, // 区分最小化和关闭状态
reconnectAttempts: 0,
maxReconnectAttempts: 3, // 减少最大重连次数
reconnectInterval: 3000, // 减少重连间隔
isReconnecting: false,
lastActivityTime: Date.now(),
activityCheckInterval: null,
networkStatus: "online",
reconnectTimer: null,
connectionError: null, // 添加错误信息存储
showRefreshButton: false, // 添加刷新按钮
heartbeatCheckInterval: 30000, // 每30秒检查一次心跳
maxMessageLength: 300,
// === 新增:连接验证相关 ===
connectionVerifyTimer: null, // 连接验证定时器
isConnectionVerified: false, // 连接是否已验证
isHandlingError: false, // 是否正在处理错误
lastErrorTime: 0, // 最后一次错误时间
lastConnectedEmail: null, // 最后连接的用户email用于防止重复连接
userViewHistory: false, // 是否在查看历史消息
customerIsOnline: true, // 保存客服在线状态
jurisdiction: {
roleKey: "",
},
};
},
computed: {
/**
* 生成带有时间分割条的消息列表
* @returns {Array} 消息和分割条混合数组
*/
displayMessages() {
const result = [];
const interval = 5 * 60 * 1000; // 5分钟
let lastTime = null;
this.messages.forEach((msg, idx) => {
if (!msg.isSystemHint && !msg.isLoading) {
const msgTime = new Date(msg.time); // 直接new即可
if (!lastTime || msgTime - lastTime > interval) {
result.push({
isTimeDivider: true,
time: msg.time, // 直接用字符串
id: `divider-${msgTime.getTime()}-${idx}`, // 使用时间戳确保唯一性
});
lastTime = msgTime;
}
}
result.push(msg);
});
return result;
},
},
// 监听状态变化
watch: {
connectionStatus(newStatus, oldStatus) {
if (newStatus !== oldStatus) {
console.log(`🔄 连接状态变化: ${oldStatus} -> ${newStatus}`);
console.log(`🔍 当前时间: ${new Date().toLocaleTimeString()}`);
console.log(`🔍 WebSocket状态: ${this.isWebSocketConnected}`);
console.log(`🔍 STOMP状态: ${this.stompClient?.connected}`);
console.log(`🔍 重连状态: ${this.isReconnecting}`);
console.log(`🔍 验证状态: ${this.isConnectionVerified}`);
// 如果状态莫名其妙变为connecting记录调用栈
if (newStatus === "connecting" && oldStatus === "connected") {
console.warn("⚠️ 连接状态从connected变为connecting可能有问题");
console.trace("调用栈:");
}
// === 新增强制触发Vue重新渲染 ===
if (newStatus === "connected") {
console.log("✅ 状态已变为connected强制触发重新渲染");
this.$forceUpdate();
}
}
},
isChatOpen(val) {
if (val) {
// 聊天框每次打开都兜底滚动到底部,提升体验
this.$nextTick(() => this.scrollToBottomOnInit());
// === 移除 markMessagesAsRead 调用,防止切换窗口时误清未读 ===
}
},
},
async created() {
let jurisdiction = localStorage.getItem("jurisdiction");
try {
jurisdiction = jurisdiction ? JSON.parse(jurisdiction) : { roleKey: "" };
} catch (e) {
jurisdiction = { roleKey: "" };
}
this.jurisdiction = jurisdiction;
window.addEventListener("setItem", () => {
let jurisdiction = localStorage.getItem("jurisdiction");
try {
jurisdiction = jurisdiction
? JSON.parse(jurisdiction)
: { roleKey: "" };
} catch (e) {
jurisdiction = { roleKey: "" };
}
this.jurisdiction = jurisdiction;
});
// === 初始化未读消息数从localStorage读取 ===
this.initUnreadMessages();
this.determineUserType();
},
mounted() {
let jurisdiction = localStorage.getItem("jurisdiction");
try {
jurisdiction = jurisdiction ? JSON.parse(jurisdiction) : { roleKey: "" };
} catch (e) {
jurisdiction = { roleKey: "" };
}
this.jurisdiction = jurisdiction;
window.addEventListener("setItem", () => {
let jurisdiction = localStorage.getItem("jurisdiction");
try {
jurisdiction = jurisdiction
? JSON.parse(jurisdiction)
: { roleKey: "" };
} catch (e) {
jurisdiction = { roleKey: "" };
}
this.jurisdiction = jurisdiction;
});
// 添加页面卸载事件监听
window.addEventListener("beforeunload", this.handleBeforeUnload);
// === 添加localStorage监听实现多窗口未读消息同步 ===
window.addEventListener("storage", this.handleStorageChange);
document.addEventListener("click", this.handleClickOutside);
// === 移除滚动监听,简化逻辑 ===
// this.$nextTick(() => {
// if (this.$refs.chatBody) {
// this.$refs.chatBody.addEventListener("scroll", this.handleChatScroll);
// }
// });
// 添加页面可见性变化监听
document.addEventListener("visibilitychange", this.handleVisibilityChange);
// 添加网络状态变化监听
window.addEventListener("online", this.handleNetworkChange);
window.addEventListener("offline", this.handleNetworkChange);
// 添加用户活动检测
this.startActivityCheck();
// 添加用户活动监听
document.addEventListener("mousemove", this.updateLastActivityTime);
document.addEventListener("keydown", this.updateLastActivityTime);
document.addEventListener("click", this.updateLastActivityTime);
// 监听退出登录事件
this.$bus.$on("user-logged-out", this.handleLogout);
// 监听登录成功
this.$bus.$on("user-logged-in", this.handleLoginSuccess);
// === 新增:添加快捷键用于调试状态 ===
this.setupDebugMode();
// 添加聊天区滚动事件监听
this.$nextTick(() => {
if (this.$refs.chatBody) {
this.$refs.chatBody.addEventListener(
"scroll",
this.handleChatBodyScroll
);
}
});
// 聊天框初始化时多次兜底滚动到底部,保证异步内容加载后滚动到位
this.scrollToBottomOnInit();
},
methods: {
// === 初始化未读消息数 ===
initUnreadMessages() {
try {
const storageKey = this.getUnreadStorageKey();
const stored = localStorage.getItem(storageKey);
this.unreadMessages = stored ? parseInt(stored, 10) || 0 : 0;
// console.log("📋 初始化未读消息数:", this.unreadMessages);
} catch (error) {
console.warn("读取未读消息数失败:", error);
this.unreadMessages = 0;
}
},
// === 获取未读消息的localStorage键名 ===
getUnreadStorageKey() {
return `chat_unread_${this.userEmail || "guest"}`;
},
// === 更新未读消息数到localStorage ===
updateUnreadMessages(count) {
try {
const storageKey = this.getUnreadStorageKey();
this.unreadMessages = count;
localStorage.setItem(storageKey, String(count));
// console.log("📝 更新未读消息数:", count);
} catch (error) {
console.warn("保存未读消息数失败:", error);
}
},
// === 监听localStorage变化同步多窗口 ===
handleStorageChange(event) {
if (event.key && event.key.startsWith("chat_unread_")) {
// 检查是否是当前用户的未读消息
const currentKey = this.getUnreadStorageKey();
if (event.key === currentKey) {
const newCount = parseInt(event.newValue, 10) || 0;
// console.log("🔄 检测到其他窗口更新未读消息数:", newCount);
this.unreadMessages = newCount;
}
}
},
// 初始化聊天系统
async initChatSystem() {
// console.log("🔧 初始化聊天系统, userEmail:", this.userEmail);
// console.log("🔍 当前连接状态:", this.connectionStatus);
// console.log("🔍 当前WebSocket状态:", this.isWebSocketConnected);
if (!this.userEmail) {
console.log("❌ userEmail为空跳过初始化");
return;
}
// === 防止重复初始化如果已经连接且使用相同email跳过 ===
if (
this.isWebSocketConnected &&
this.connectionStatus === "connected" &&
this.userEmail === this.lastConnectedEmail
) {
console.log("✅ 聊天系统已初始化且连接正常,跳过重复初始化");
return;
}
try {
console.log("jurisdict口服空手道咖啡豆防控ion", this.jurisdiction);
if (
this.jurisdiction.roleKey =="customer_service" ||
this.jurisdiction.roleKey == "admin" ||
this.jurisdiction.roleKey == "back_admin"
) return;
// 获取用户ID和未读消息数
const userData = await this.fetchUserid({ email: this.userEmail });
if (userData) {
this.roomId = userData.id;
this.receivingEmail = userData.userEmail;
// === 保存客服在线状态 ===
this.customerIsOnline = userData.customerIsOnline;
// === 使用localStorage管理未读消息数 ===
this.updateUnreadMessages(userData.clientReadNum || 0);
// === 只有在未连接或email变化时才初始化连接 ===
if (
!this.isWebSocketConnected ||
this.userEmail !== this.lastConnectedEmail
) {
// console.log("🔄 需要建立新连接, 用户:", userData.selfEmail);
// 如果有旧连接且email不同先断开
if (
this.isWebSocketConnected &&
this.userEmail !== this.lastConnectedEmail
) {
// console.log("🔄 用户身份变化,断开旧连接");
await this.forceDisconnectAll();
}
this.initWebSocket(userData.selfEmail);
this.lastConnectedEmail = this.userEmail; // 记录当前连接的email
} else {
// console.log("✅ WebSocket已连接复用现有连接");
}
}
} catch (error) {
console.error("初始化聊天系统失败:", error);
}
},
// 初始化 WebSocket 连接
initWebSocket(selfEmail) {
// this.determineUserType();
this.connectWebSocket(selfEmail);
},
// 确定用户类型和邮箱
async determineUserType() {
try {
const token = localStorage.getItem("token");
// console.log("token", token);
if (!token) {
// === 游客身份检查是否已有缓存的游客email ===
const cachedGuestEmail = sessionStorage.getItem("chatGuestEmail");
if (cachedGuestEmail && cachedGuestEmail.startsWith("guest_")) {
// console.log("📋 复用已缓存的游客身份:", cachedGuestEmail);
this.userType = 0;
this.userEmail = cachedGuestEmail;
} else {
// 生成新的游客身份
this.userType = 0;
this.userEmail = `guest_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
// 缓存到sessionStorage页面刷新前保持不变
sessionStorage.setItem("chatGuestEmail", this.userEmail);
// console.log("🆕 生成新游客用户:", this.userEmail);
}
// 页面加载时立即获取用户信息
this.initChatSystem();
return;
}
try {
const userInfo = JSON.parse(
localStorage.getItem("jurisdiction") || "{}"
);
// const email = JSON.parse(localStorage.getItem("userEmail") || "{}");
const emailData = localStorage.getItem("userEmail") || "{}";
let email = "";
try {
const emailObj = JSON.parse(emailData);
// 如果是对象,尝试取常见的邮箱字段
email =
emailObj.email ||
emailObj.value ||
emailObj.userEmail ||
emailObj;
// 如果最终结果不是字符串,可能数据有问题
if (typeof email !== "string") {
email = "";
}
} catch (e) {
// 如果 JSON.parse 失败,说明可能就是字符串
email = emailData;
}
this.userEmail = email;
if (userInfo.roleKey === "customer_service") {
// 客服用户
this.userType = 2;
this.userEmail = "";
} else {
// 登录用户
this.userType = 1;
this.userEmail = email;
}
// === 用户身份确定后,重新初始化未读消息数 ===
this.initUnreadMessages();
// 页面加载时立即获取用户信息
await this.initChatSystem();
} catch (parseError) {
console.error("解析用户信息失败:", parseError);
// === 解析失败时使用游客身份,复用缓存 ===
this.setupGuestIdentity();
}
} catch (error) {
console.error("获取用户信息失败:", error);
// === 出错时使用游客身份,复用缓存 ===
this.setupGuestIdentity();
}
},
/**
* 设置游客身份复用已缓存的email
*/
setupGuestIdentity() {
const cachedGuestEmail = sessionStorage.getItem("chatGuestEmail");
if (cachedGuestEmail && cachedGuestEmail.startsWith("guest_")) {
// console.log("📋 异常处理时复用已缓存的游客身份:", cachedGuestEmail);
this.userType = 0;
this.userEmail = cachedGuestEmail;
} else {
this.userType = 0;
this.userEmail = `guest_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
sessionStorage.setItem("chatGuestEmail", this.userEmail);
// console.log("🆕 异常处理时生成新游客身份:", this.userEmail);
}
// 初始化聊天系统
this.initChatSystem();
},
// 添加订阅消息的方法
subscribeToPersonalMessages(selfEmail) {
// console.log("🔗 开始订阅流程selfEmail:", selfEmail);
// console.log("🔍 订阅前状态检查:", {
// stompClient: !!this.stompClient,
// stompConnected: this.stompClient?.connected,
// isWebSocketConnected: this.isWebSocketConnected,
// connectionStatus: this.connectionStatus,
// });
if (!this.stompClient || !this.isWebSocketConnected) {
console.error("❌ STOMP客户端未连接无法订阅消息");
this.connectionStatus = "error";
this.connectionError = this.$t("chat.unableToSubscribe"); //连接状态异常,无法订阅消息
this.isWebSocketConnected = false;
this.showRefreshButton = false; // 连接状态异常通常可以重试解决
this.$forceUpdate();
return;
}
if (!this.stompClient.connected) {
console.error("❌ STOMP客户端已断开无法订阅消息");
this.connectionStatus = "error";
this.connectionError = "chat.unableToSubscribe"; //连接已断开,无法订阅消息
this.isWebSocketConnected = false;
this.showRefreshButton = false; // 连接断开通常可以重试解决
this.$forceUpdate();
return;
}
try {
// console.log("🔗 开始订阅消息频道:", `/sub/queue/user/${selfEmail}`);
// 订阅个人消息频道
// console.log("🔗 调用 stompClient.subscribe...");
// console.log("🔍 订阅目标:", `/sub/queue/user/${selfEmail}`);
// console.log("🔍 STOMP客户端状态:", this.stompClient?.connected);
const subscription = this.stompClient.subscribe(
`/sub/queue/user/${selfEmail}`,
(message) => {
// console.log("📨 收到消息,标记连接已验证");
// 更新最后心跳时间
this.lastHeartbeatTime = Date.now();
// === 强制确保连接状态正确 ===
if (this.connectionStatus !== "connected") {
// console.log("🔧 收到消息时发现状态不对强制修正为connected");
this.connectionStatus = "connected";
this.isWebSocketConnected = true;
this.isReconnecting = false;
this.connectionError = null;
}
// === 标记连接已验证 ===
this.markConnectionVerified();
// 处理消息
this.onMessageReceived(message);
}
);
// console.log("🔍 订阅调用完成subscription:", subscription);
// console.log("🔍 subscription类型:", typeof subscription);
// console.log("🔍 subscription.id:", subscription?.id);
// console.log(
// "🔍 subscription是否为有效对象:",
// !!subscription && typeof subscription === "object"
// );
// === 关键修复:立即设置为连接状态,不等待消息到达 ===
// console.log("🚀 立即设置连接状态为connected解决卡顿问题");
this.connectionStatus = "connected";
this.isWebSocketConnected = true;
this.isReconnecting = false;
this.connectionError = null;
this.reconnectAttempts = 0;
this.markConnectionVerified();
this.$forceUpdate();
// === 启动活动检测和完成设置 ===
// console.log("✅ 订阅设置完成,启动活动检测");
this.startActivityCheck();
// console.log("🔍 订阅最终状态检查:", {
// connectionStatus: this.connectionStatus,
// isWebSocketConnected: this.isWebSocketConnected,
// isReconnecting: this.isReconnecting,
// isConnectionVerified: this.isConnectionVerified,
// reconnectAttempts: this.reconnectAttempts,
// });
} catch (error) {
console.error("❌ 订阅消息异常:", error);
// console.log("🔍 订阅异常详情:", error.message);
// === 订阅异常立即设置错误状态 ===
this.connectionStatus = "error";
this.connectionError = this.$t("chat.conflict"); //连接异常,可能是多窗口冲突,请关闭其他窗口重试
this.isWebSocketConnected = false;
this.isReconnecting = false;
this.showRefreshButton = false; // 多窗口冲突不需要刷新页面,重试即可
// console.log("🔥 订阅异常,立即设置错误状态");
this.$forceUpdate();
}
},
// 连接 WebSocket
async connectWebSocket(selfEmail) {
// 健壮恢复userEmail
let email = selfEmail || this.userEmail;
// 1. 优先从 localStorage 查找
if (!email) {
try {
const emailData = localStorage.getItem("userEmail");
if (emailData) {
const emailObj = JSON.parse(emailData);
email =
emailObj.email ||
emailObj.value ||
emailObj.userEmail ||
emailObj;
if (typeof email !== "string") email = "";
}
} catch (e) {
// 解析失败时忽略,继续后续逻辑
console.warn("[DEBUG] 解析localStorage userEmail失败:", e);
}
}
// 2. 再从 sessionStorage 查找游客邮箱
if (!email) {
const guestEmail = sessionStorage.getItem("chatGuestEmail");
if (guestEmail && guestEmail.startsWith("guest_")) {
email = guestEmail;
}
}
// 3. 兜底生成游客邮箱
if (!email) {
email = `guest_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
sessionStorage.setItem("chatGuestEmail", email);
console.warn("[DEBUG] 自动生成游客邮箱:", email);
}
this.userEmail = email;
selfEmail = email;
// console.log("[DEBUG] connectWebSocket called", {
// isWebSocketConnected: this.isWebSocketConnected,
// isReconnecting: this.isReconnecting,
// lastConnectedEmail: this.lastConnectedEmail,
// selfEmail,
// userEmail: this.userEmail,
// connectionStatus: this.connectionStatus,
// });
if (!selfEmail) {
// console.warn("[DEBUG] connectWebSocket: 缺少用户邮箱参数");
return Promise.reject(new Error("缺少用户邮箱参数"));
}
if (this.isWebSocketConnected && this.lastConnectedEmail === selfEmail) {
// console.log("[DEBUG] connectWebSocket: 已连接,复用");
return Promise.resolve("already_connected");
}
if (this.isReconnecting) {
// console.log("[DEBUG] connectWebSocket: 正在重连中,跳过");
return Promise.resolve("reconnecting");
}
this.connectionStatus = "connecting";
this.isReconnecting = true;
this.connectionError = null;
// === 新增:设置连接超时检测 ===
const connectionTimeout = setTimeout(() => {
if (
this.connectionStatus === "connecting" &&
!this.isConnectionVerified
) {
// console.log("连接超时30秒强制断开重连");
// console.log("🔍 超时时状态检查:", {
// connectionStatus: this.connectionStatus,
// isWebSocketConnected: this.isWebSocketConnected,
// isConnectionVerified: this.isConnectionVerified,
// stompConnected: this.stompClient?.connected,
// });
this.handleConnectionTimeout();
} else {
// console.log("连接超时检查:连接已验证或状态已变化,跳过超时处理");
}
}, 30000); // 缩短到30秒超时
try {
const apiUrl = process.env.VUE_APP_BASE_API;
let baseUrl=""
// 将 https 替换为 wss
if (apiUrl.startsWith("https://")) {
baseUrl= apiUrl.replace("https://", "wss://");
}
if (apiUrl.startsWith("http://")) {
baseUrl=apiUrl.replace("http://", "ws://");
}
const wsUrl = `${baseUrl}chat/ws`;
// console.log(wsUrl,"接地极低等级点击都觉得点击都觉得点击的的的的记得到点击");
// === 彻底释放旧的stompClient和WebSocket对象 ===
if (this.stompClient) {
try {
this.stompClient.disconnect();
// console.log("[DEBUG] 旧stompClient已disconnect");
} catch (e) {
console.warn("[DEBUG] stompClient.disconnect异常", e);
}
this.stompClient = null;
}
// === 新建连接前详细日志 ===
// console.log("[DEBUG] 即将新建stompClient:", wsUrl);
this.stompClient = Stomp.client(wsUrl);
// console.log("[DEBUG] stompClient对象已创建:", this.stompClient);
// === 新增设置WebSocket连接超时 ===
this.stompClient.webSocketFactory = () => {
const ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer";
// 设置WebSocket级别的错误处理
ws.onerror = (error) => {
console.error("WebSocket连接错误:", error);
clearTimeout(connectionTimeout);
this.handleWebSocketError(error);
};
// 监听WebSocket状态变化
ws.onopen = () => {
// console.log("WebSocket连接已建立");
};
ws.onclose = (event) => {
// console.log("WebSocket连接已关闭:", event.code, event.reason);
clearTimeout(connectionTimeout);
if (!this.isReconnecting) {
this.handleWebSocketClose(event);
}
};
return ws;
};
const headers = {
email: selfEmail,
type: this.userType,
};
// 修改错误处理
this.stompClient.onStompError = (frame) => {
// 只用frame.headers.message判断
const errorMessage = frame.headers?.message || "";
console.error("🔴 STOMP 错误:", errorMessage);
// 只要包含1020就处理
if (errorMessage.includes("1020")) {
this.handleConnectionLimitError();
return;
}
// 其他错误可选处理
this.connectionError = errorMessage || this.$t("chat.abnormal"); //连接异常
this.connectionStatus = "error";
this.isWebSocketConnected = false;
this.isReconnecting = false;
this.showRefreshButton = true;
this.$forceUpdate();
};
return new Promise((resolve, reject) => {
this.stompClient.connect(
headers,
(frame) => {
// console.log("🎉 WebSocket Connected:", frame);
clearTimeout(connectionTimeout);
this.isWebSocketConnected = true;
this.connectionStatus = "connecting"; // 保持connecting状态直到订阅完成
this.reconnectAttempts = 0;
this.isReconnecting = false;
this.connectionError = null;
// console.log("🔗 开始订阅个人消息...");
this.subscribeToPersonalMessages(selfEmail);
this.startHeartbeat();
// === 注意:不在这里启动验证,而是在订阅成功后 ===
// console.log("⚡ 连接成功,等待订阅完成后验证");
// === 设置订阅超时检查缩短到5秒 ===
setTimeout(() => {
if (
this.connectionStatus === "connecting" &&
!this.isConnectionVerified
) {
console.warn(
"⚠️ 连接成功但5秒内未完成订阅验证可能是多窗口冲突或订阅失败"
);
// console.log("🔍 订阅超时时的状态:", {
// connectionStatus: this.connectionStatus,
// isWebSocketConnected: this.isWebSocketConnected,
// isConnectionVerified: this.isConnectionVerified,
// stompConnected: this.stompClient?.connected,
// });
this.connectionStatus = "error";
this.connectionError = this.$t("chat.conflict"); //订阅失败,可能是多窗口冲突,请关闭其他窗口重试
this.isWebSocketConnected = false;
this.isReconnecting = false;
this.showRefreshButton = true;
this.$forceUpdate();
}
}, 5000); // 增加到5秒给订阅更多时间
resolve(frame);
},
(error) => {
console.error("WebSocket Error:", error);
clearTimeout(connectionTimeout);
// === 新增:处理特殊的握手错误 ===
if (this.isHandshakeError(error)) {
this.handleHandshakeError(error);
reject(error);
return;
}
// 检查是否是连接数上限错误
if (this.isConnectionLimitError(error.headers.message)) {
this.connectionError = error.headers.message;
this.connectionStatus = "error"; // 立即设置为错误状态
this.isReconnecting = false;
this.handleConnectionLimitError();
reject(error);
return;
}
// === 简化连接错误消息,避免技术细节 ===
if (error.headers.message.includes("503")) {
this.connectionError = this.$t("chat.server500"); // "服务器暂时不可用,请稍后重试";
} else if (error.headers.message.includes("handshake")) {
this.connectionError = this.$t("chat.networkAnomaly"); //网络连接异常,正在重试
} else {
this.connectionError = this.$t(`chat.abnormal`); //连接异常,正在重试
}
this.isReconnecting = false;
this.handleDisconnect();
reject(error);
}
);
// 设置 STOMP 心跳
this.stompClient.heartbeat.outgoing = 30000; // 30秒发送一次心跳
this.stompClient.heartbeat.incoming = 30000; // 30秒接收一次心跳
});
} catch (error) {
console.error("初始化 WebSocket 失败:", error);
clearTimeout(connectionTimeout);
this.connectionError = this.$t("chat.initializationFailed"); //初始化失败,请刷新页面重试
this.isReconnecting = false;
this.handleDisconnect();
return Promise.reject(error);
}
},
// 添加新重连最多重连5次
handleDisconnect() {
// console.log("[DEBUG] handleDisconnect", {
// isWebSocketConnected: this.isWebSocketConnected,
// isReconnecting: this.isReconnecting,
// reconnectAttempts: this.reconnectAttempts,
// connectionStatus: this.connectionStatus,
// });
if (this.isReconnecting) return;
// console.log("🔌 处理连接断开...");
// === 如果用户已经打开聊天窗口,确保保持打开状态 ===
// if (this.isChatOpen) {
// console.log("📱 聊天窗口已打开,保持打开状态");
// // 不改变 isChatOpen 和 isMinimized 状态
// }
this.isWebSocketConnected = false;
this.connectionStatus = "error";
this.isReconnecting = true;
// === 新增:清除连接验证定时器 ===
this.clearConnectionVerification();
// 清除之前的重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
// ===检查是否是连接数上限错误 ===
if (this.isConnectionLimitError(this.connectionError)) {
// 如果是连接数上限错误,直接调用专门的处理方法
this.handleConnectionLimitError();
return;
}
// ===统一处理其他类型的连接错误 ===
if (this.handleConnectionError(this.connectionError)) {
return;
}
// === 新增结束 ===
// 使用现有的重连逻辑
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
// console.log(
// `🔄 尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`
// );
// === 在自动重连期间,如果聊天窗口打开,显示连接中状态 ===
if (this.isChatOpen) {
this.connectionStatus = "connecting";
}
// === 移除自动重连提示:后台静默处理,不打扰用户 ===
// console.log(
// `🔄 自动重连中 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`
// );
// 只记录日志不显示toast提示
this.reconnectTimer = setTimeout(() => {
this.isReconnecting = false; // 兜底重置,确保重连能进入
if (!this.isWebSocketConnected) {
const connectionPromise = this.connectWebSocket(this.userEmail);
if (
connectionPromise &&
typeof connectionPromise.catch === "function"
) {
connectionPromise.catch((error) => {
console.error("[DEBUG] 自动重连失败:", error);
});
}
}
}, this.reconnectInterval);
} else {
// console.log("❌ 达到最大重连次数,停止重连");
// === 只在达到最大重连次数时才提示用户,因为需要用户手动刷新 ===
// this.$message.error(this.$t("chat.connectionFailed") || "连接异常,请刷新页面重试");
this.isReconnecting = false;
this.showRefreshButton = true;
}
},
// 处理网络状态变化
handleNetworkChange() {
this.networkStatus = navigator.onLine ? "online" : "offline";
// console.log("[DEBUG] handleNetworkChange", {
// online: navigator.onLine,
// isWebSocketConnected: this.isWebSocketConnected,
// isReconnecting: this.isReconnecting,
// connectionStatus: this.connectionStatus,
// });
if (navigator.onLine) {
// === 强制重置状态,兜底 ===
location.reload(); // 重新加载当前页面
this.isChatOpen = false;
this.isMinimized = false;
this.isLoadingHistory = false;
this.isLoading = false;
this.isWebSocketConnected = false;
this.isReconnecting = false;
// if (!this.isWebSocketConnected) {
// console.log('[DEBUG] 网络恢复,触发 handleDisconnect');
// this.handleDisconnect();
// }
}
},
// 开始活动检测
startActivityCheck() {
this.activityCheckInterval = setInterval(() => {
const now = Date.now();
const inactiveTime = now - this.lastActivityTime;
// 如果用户超过5分钟没有活动且连接断开则尝试重连
if (inactiveTime > 5 * 60 * 1000 && !this.isWebSocketConnected) {
this.handleDisconnect();
}
}, 60000); // 每分钟检查一次
},
// 更新最后活动时间
updateLastActivityTime() {
this.lastActivityTime = Date.now();
},
// 页面关闭时的处理
handleBeforeUnload() {
this.disconnectWebSocket();
},
//只能输入300个字符
handleInputMessage() {
if (this.inputMessage.length > this.maxMessageLength) {
this.inputMessage = this.inputMessage.slice(0, this.maxMessageLength);
}
},
/**
* 处理Enter键事件
* @param {KeyboardEvent} event - 键盘事件对象
*/
handleEnterKey(event) {
// 检查是否按下了Enter键不包含Shift+Enter
if (event.key === "Enter" && !event.shiftKey) {
// 阻止默认的换行行为
event.preventDefault();
// 检查是否有内容可以发送
if (this.inputMessage.trim() && this.connectionStatus === "connected") {
this.sendMessage();
}
}
// 如果是Shift+Enter允许默认行为换行
// 注意当前使用的是input标签不支持多行如果需要支持Shift+Enter换行
// 应该将input改为textarea
},
// 发送消息
sendMessage() {
// 网络断开时阻止发送消息并提示
if (this.networkStatus !== "online") {
this.$message({
message:
this.$t("chat.networkError") || "网络连接已断开,无法发送消息",
type: "error",
showClose: true,
});
return;
}
// === 游客且客服离线时提示 ===
if (this.userType === 0 && this.customerIsOnline === false) {
this.$message({
message:
this.$t("chat.customerServiceOffline") ||
"客服离线,请登录账号发送留言消息",
type: "warning",
showClose: true,
});
return;
}
if (!this.inputMessage.trim()) {
this.$message({
message: this.$t("chat.sendMessageEmpty") || "发送消息不能为空",
type: "warning",
showClose: true,
});
return;
}
if (this.inputMessage.length > this.maxMessageLength) {
this.$message.warning(
this.$t("chat.contentMax") ||
"超出发送内容大小限制,请删除部分内容(300字以内)"
); //超出发送内容大小限制,请删除部分内容 300个字符
return;
}
// 检查 WebSocket 连接状态
if (!this.stompClient || !this.stompClient.connected) {
// console.log("发送消息时连接已断开,尝试重连...");
// === 移除重连提示:会自动重连,不需要打扰用户 ===
this.handleDisconnect();
return;
}
const messageText = this.inputMessage.trim();
// === 立即本地显示机制 ===
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
const localMessageId = `local_${timestamp}_${random}`;
// 创建本地消息对象
const localMessage = {
id: localMessageId,
content: messageText,
type: 1,
sendEmail: this.userEmail,
sendTime: new Date().toISOString(),
roomId: this.roomId,
isLocalMessage: true, // 标记为本地消息
};
// 立即添加到本地聊天记录
// console.log("📤 立即显示本地消息:", localMessage);
this.addMessageToChat(localMessage, true);
try {
const message = {
content: messageText,
type: 1, // 1 表示文字消息
email: this.receivingEmail,
receiveUserType: 2,
roomId: this.roomId,
};
// 发送消息到服务器
this.stompClient.send(
"/point/send/message/to/customer",
{},
JSON.stringify(message)
);
this.inputMessage = "";
} catch (error) {
console.error("发送消息失败:", error);
// === 优化发送失败提示:只在明确需要用户重试时提示 ===
this.$message.error(this.$t("chat.failInSend") || "发送失败,请重试");
}
},
// 断开 WebSocket 连接
disconnectWebSocket() {
this.stopHeartbeat();
// === 新增:清除连接验证定时器 ===
this.clearConnectionVerification();
if (this.stompClient) {
try {
// 取消所有订阅
if (this.stompClient.subscriptions) {
Object.keys(this.stompClient.subscriptions).forEach((id) => {
this.stompClient.unsubscribe(id);
});
}
// 断开连接
this.stompClient.deactivate();
this.isWebSocketConnected = false;
this.connectionStatus = "disconnected";
this.reconnectAttempts = 0;
this.isReconnecting = false;
} catch (error) {
console.error("断开 WebSocket 连接失败:", error);
}
}
},
// 处理页面可见性变化
// 处理页面可见性变化
handleVisibilityChange() {
// 当页面变为可见且聊天窗口已打开时,标记消息为已读
// if (!document.hidden && this.isChatOpen && this.roomId) {
// this.markMessagesAsRead();
// }
// 添加新的重连逻辑
if (!document.hidden) {
// 页面变为可见时,检查连接状态
if (!this.isWebSocketConnected) {
this.handleDisconnect();
}
// 更新最后活动时间
this.updateLastActivityTime();
}
},
// 修改标记消息为已读的方法
async markMessagesAsRead() {
if (!this.roomId || !this.userEmail) {
console.log("缺少必要参数,跳过标记已读");
return;
}
try {
const data = {
roomId: this.roomId,
userType: this.userType,
email: this.userEmail,
};
// 添加重试机制
let retryCount = 0;
const maxRetries = 3;
let success = false;
while (retryCount < maxRetries && !success) {
try {
const response = await getReadMessage(data);
if (response && response.code === 200) {
// console.log("消息已标记为已读");
// === 使用localStorage管理未读消息数 ===
this.updateUnreadMessages(0);
// 更新所有用户消息的已读状态
this.messages.forEach((msg) => {
if (msg.type === "user") {
msg.isRead = true;
}
});
success = true;
} else {
console.warn(
`标记消息已读失败 (尝试 ${retryCount + 1}/${maxRetries}):`,
response
);
retryCount++;
if (retryCount < maxRetries) {
// 等待一段时间后重试
await new Promise((resolve) =>
setTimeout(resolve, 1000 * retryCount)
);
}
}
} catch (error) {
// console.error(
// `标记消息已读出错 (尝试 ${retryCount + 1}/${maxRetries}):`,
// error
// );
retryCount++;
if (retryCount < maxRetries) {
// 等待一段时间后重试
await new Promise((resolve) =>
setTimeout(resolve, 1000 * retryCount)
);
}
}
}
if (!success) {
console.warn("标记消息已读失败,已达到最大重试次数");
// 即使标记已读失败,也更新本地状态
this.updateUnreadMessages(0);
this.messages.forEach((msg) => {
if (msg.type === "user") {
msg.isRead = true;
}
});
}
} catch (error) {
console.error("标记消息已读出错:", error);
// 即使出错,也更新本地状态
this.updateUnreadMessages(0);
this.messages.forEach((msg) => {
if (msg.type === "user") {
msg.isRead = true;
}
});
}
},
// 加载历史消息
async loadHistoryMessages() {
if (this.isLoadingHistory || !this.roomId) return;
this.isLoadingHistory = true;
try {
const response = await getHistory7({
roomId: this.roomId,
userType: this.userType,
email: this.userEmail,
});
// console.log("📋 初始历史消息加载响应:", {
// code: response?.code,
// dataExists: !!response?.data,
// dataLength: response?.data?.length || 0,
// isArray: Array.isArray(response?.data),
// });
if (response?.code === 200 && Array.isArray(response.data)) {
// 使用统一的格式化方法
const historyMessages = this.formatHistoryMessages(response.data);
if (historyMessages.length > 0) {
// 有历史消息,按时间顺序排序
this.messages = historyMessages.sort(
(a, b) => new Date(a.time) - new Date(b.time)
);
// console.log(
// "✅ 成功加载",
// historyMessages.length,
// "条初始历史消息"
// );
// 保持对话框打开状态
this.isChatOpen = true;
this.isMinimized = false;
// 只有在初始加载历史消息时才滚动到底部(当消息列表为空时)
await this.$nextTick();
// 添加一个小延时确保所有内容都渲染完成
setTimeout(() => {
this.scrollToBottom(true); // 传入 true 表示强制滚动
}, 100);
} else {
// 格式化后没有有效消息
this.messages = [
{
type: "system",
text: this.$t("chat.noHistory") || "暂无历史消息",
isSystemHint: true,
time: new Date().toISOString(),
},
];
// console.log("📋 初始历史消息为空(格式化后无有效消息)");
}
} else {
// 响应无效或无数据
this.messages = [
{
type: "system",
text: this.$t("chat.noHistory") || "暂无历史消息",
isSystemHint: true,
time: new Date().toISOString(),
},
];
// console.log("📋 初始历史消息为空(响应无效)");
}
} catch (error) {
console.error("加载历史消息失败:", error);
// === 简化历史消息加载失败提示 ===
this.$message.error(
this.$t("chat.loadHistoryFailed") || "加载历史消息失败"
);
this.messages = [
{
type: "system",
text: "加载历史消息失败,请重试",
isSystemHint: true,
time: new Date().toISOString(),
isError: true,
},
];
} finally {
this.isLoadingHistory = false;
}
},
// === 简化的历史消息加载逻辑 ===
async loadMoreHistory() {
if (this.isLoadingHistory || !this.roomId) return;
this.isLoadingHistory = true;
try {
// === 记录当前第一个可见消息的DOM和offsetTop ===
let prevFirstMsgId = null;
let prevFirstMsgOffset = 0;
const chatBody = this.$refs.chatBody;
if (chatBody && chatBody.children && chatBody.children.length > 0) {
for (let i = 0; i < chatBody.children.length; i++) {
const el = chatBody.children[i];
if (el.classList.contains("chat-message")) {
prevFirstMsgId = this.messages[i]?.id;
prevFirstMsgOffset = el.offsetTop;
break;
}
}
}
const oldestMessage = this.messages.find(
(msg) => !msg.isSystemHint && !msg.isLoading
);
const response = await getHistory7({
roomId: this.roomId,
userType: this.userType,
email: this.userEmail,
id: oldestMessage?.id,
});
if (
response &&
response.code === 200 &&
response.data &&
Array.isArray(response.data) &&
response.data.length > 0
) {
const historyMessages = this.formatHistoryMessages(response.data);
if (historyMessages.length > 0) {
const existingIds = new Set(
this.messages.map((msg) => msg.id).filter((id) => id)
);
const newMessages = historyMessages.filter(
(msg) => !existingIds.has(msg.id)
);
if (newMessages.length > 0) {
this.messages = [...newMessages, ...this.messages];
this.$nextTick(() => {
if (chatBody && prevFirstMsgId) {
let newIndex = this.messages.findIndex(
(msg) => msg.id === prevFirstMsgId
);
if (newIndex !== -1 && chatBody.children[newIndex]) {
const newOffset = chatBody.children[newIndex].offsetTop;
chatBody.scrollTop = newOffset - prevFirstMsgOffset;
}
}
});
} else {
this.hasMoreHistory = false;
this.messages.unshift({
type: "system",
text: this.$t("chat.noMoreHistory") || "没有更多历史消息了",
isSystemHint: true,
time: new Date().toISOString(),
});
}
} else {
this.hasMoreHistory = false;
this.messages.unshift({
type: "system",
text: this.$t("chat.noMoreHistory") || "没有更多历史消息了",
isSystemHint: true,
time: new Date().toISOString(),
});
}
} else {
this.hasMoreHistory = false;
this.messages.unshift({
type: "system",
text: this.$t("chat.noMoreHistory") || "没有更多历史消息了",
isSystemHint: true,
time: new Date().toISOString(),
});
}
} catch (error) {
this.$message.error(
this.$t("chat.loadHistoryFailed") || "加载历史消息失败,请重试"
);
} finally {
this.isLoadingHistory = false;
}
},
// === 简化的历史消息格式化函数 ===
formatHistoryMessages(messagesData) {
if (
!messagesData ||
!Array.isArray(messagesData) ||
messagesData.length === 0
) {
return [];
}
const formattedMessages = messagesData
.filter((msg) => {
// 简单过滤必须有ID和内容图片消息除外
return msg && msg.id && (msg.content || msg.type === 2);
})
.map((msg) => ({
type: msg.isSelf === 1 ? "user" : "system",
text: msg.content || "",
isImage: msg.type === 2,
imageUrl: msg.type === 2 ? msg.content : null,
time:
typeof msg.createTime === "string"
? msg.createTime
: msg.createTime
? new Date(msg.createTime).toISOString()
: new Date().toISOString(),
id: msg.id,
roomId: msg.roomId,
sender: msg.sendEmail,
isHistory: true,
isRead: true,
}))
.sort((a, b) => {
// === 使用与客服系统相同的排序逻辑ID + 时间 ===
// 1. 首先按ID排序如果都有ID
if (a.id && b.id) {
const idDiff = parseInt(a.id) - parseInt(b.id);
if (idDiff !== 0) return idDiff;
}
// 2. 然后按时间排序
const aTime = new Date(a.time).getTime();
const bTime = new Date(b.time).getTime();
return aTime - bTime;
});
console.log("✅ 格式化历史消息完成,数量:", formattedMessages.length);
return formattedMessages;
},
// 修改 fetchUserid 方法,添加 token 检查
async fetchUserid(params) {
try {
// 先检查是否有 token
// const token = localStorage.getItem("token");
// if (!token) {
// console.log("用户未登录,不发起 getUserid 请求");
// // 对于未登录用户,可以生成一个临时 ID
// this.roomId = `guest_${Date.now()}`;
// this.receivingEmail = "customer_service@example.com"; // 或默认客服邮箱
// return null;
// }
const res = await getUserid(params);
if (res && res.code == 200) {
// console.log("获取用户ID成功:", res);
this.receivingEmail = res.data.userEmail;
this.roomId = res.data.id;
return res.data;
} else {
console.warn("获取用户ID未返回有效数据");
return null;
}
} catch (error) {
console.error("获取用户ID失败:", error);
throw error;
}
},
// 添加新方法:更新消息已读状态
updateMessageReadStatus(messageIds) {
if (!Array.isArray(messageIds) || messageIds.length === 0) {
// 如果没有具体的消息ID就更新所有用户消息为已读
this.messages.forEach((msg) => {
if (msg.type === "user") {
msg.isRead = true;
}
});
} else {
// 更新指定ID的消息为已读
this.messages.forEach((msg) => {
if (msg.id && messageIds.includes(msg.id)) {
msg.isRead = true;
}
});
}
},
// 接收消息处理
onMessageReceived(message) {
try {
const data = JSON.parse(message.body);
// console.log("收到新消息:", data);
// === 新增:标记连接已验证 ===
this.markConnectionVerified();
// === 新增:过滤系统验证消息,不显示在对话框中 ===
if (
data.type === 99 ||
(data.content &&
(data.content.includes("__SYSTEM_PING__") ||
data.content.includes("connection_test_ping") ||
data.content.includes("SYSTEM_PING")))
) {
// console.log("收到系统验证消息,跳过显示");
return;
}
// === 新增:处理回环消息 ===
this.handleIncomingMessage(data);
} catch (error) {
console.error("处理消息失败:", error);
}
},
/**
* 处理接收到的消息(包括回环处理)
* @param {Object} data - 消息数据
*/
handleIncomingMessage(data) {
// 判断是否是自己发送的消息(回环消息)
const isSentByMe = data.sendEmail === this.userEmail;
// console.log(`📨 处理消息: ${isSentByMe ? "自己发送的" : "对方发送的"}`, {
// sendEmail: data.sendEmail,
// userEmail: this.userEmail,
// messageId: data.id,
// });
// === 创建标准化的消息数据对象,与客服页面保持一致 ===
const messageData = {
id: data.id,
sender: data.sendEmail,
content: data.content,
// === 修复保持原始字段让createMessageObject方法处理时间格式 ===
createTime: data.createTime, // 后端返回的时间,格式如"2025-06-12T03:23:52"
sendTime: data.sendTime, // 发送时的时间
type: data.type,
roomId: data.roomId,
sendEmail: data.sendEmail,
isImage: data.type === 2,
clientReadNum: data.clientReadNum,
isLocalMessage: data.isLocalMessage || false,
};
// === 服务器消息确认机制 ===
if (isSentByMe) {
// 查找对应的本地消息并更新
const localMessageIndex = this.messages.findIndex((msg) => {
if (!msg.isLocalMessage) return false;
// 检查内容是否相同
const contentMatch = msg.text === messageData.content;
// === 修复:兼容不同的时间字段格式 ===
const msgTime = new Date(msg.time);
// 从messageData中获取时间优先使用createTime
const serverTime = messageData.createTime || messageData.sendTime;
if (!serverTime) return contentMatch; // 如果没有时间信息,只匹配内容
const dataTime = new Date(serverTime);
const timeDiff = Math.abs(dataTime - msgTime);
const timeMatch = timeDiff < 30000; // 30秒内
return contentMatch && timeMatch;
});
if (localMessageIndex !== -1) {
// console.log("🔄 找到对应本地消息,更新为服务器消息:", {
// localId: this.messages[localMessageIndex].id,
// serverId: messageData.id,
// });
// 更新本地消息为服务器消息,与客服页面保持一致
// === 修复使用createMessageObject确保时间格式正确 ===
const serverMessageObj = this.createMessageObject(messageData);
this.$set(this.messages, localMessageIndex, {
...this.messages[localMessageIndex],
id: messageData.id,
time: serverMessageObj.time, // 使用格式化后的时间
isLocalMessage: false,
});
return; // 不需要添加新消息
}
}
// 检查是否已经存在相同的消息(防止重复)
if (this.checkDuplicateMessage(messageData)) {
// console.log("⚠️ 发现重复消息,跳过添加");
return;
}
// 添加消息到聊天列表
this.addMessageToChat(messageData, isSentByMe);
// === 新增未读数逻辑 ===
if (!isSentByMe) {
// 聊天框打开且在底部,自动已读
if (this.isChatOpen && this.isAtBottom()) {
this.updateUnreadMessages(0);
// 可选:自动标记已读
// this.markMessagesAsRead();
} else {
// 聊天框未打开或不在底部,显示未读数
if (data.clientReadNum !== undefined) {
this.updateUnreadMessages(data.clientReadNum);
} else {
this.updateUnreadMessages(this.unreadMessages + 1);
}
// 显示消息通知
const messageObj = this.createMessageObject(data);
this.showNotification(messageObj);
}
}
},
/**
* 检查是否存在重复消息
* === 修复:参考客服页面,只检查回环消息的重复,避免误判对方快速发送的相同消息 ===
* @param {Object} data - 消息数据
* @returns {boolean} 是否重复
*/
checkDuplicateMessage(data) {
const timeValue = data.time || data.createTime || data.sendTime;
if (!timeValue) return false;
const messageContent = data.content;
const sendEmail = data.sendEmail;
const messageId = data.id;
// === 与客服页面保持一致如果有相同ID直接判定为重复 ===
if (messageId && this.messages.some((msg) => msg.id === messageId)) {
// console.log("🔍 发现相同ID的消息判定为重复:", messageId);
return true;
}
// === 关键修复:只检查自己发送的消息的重复(回环消息) ===
// 这样可以避免误判对方快速发送的相同内容消息
const isSentByMe = sendEmail === this.userEmail;
if (!isSentByMe) {
// 对方发送的消息不做重复检查,让其正常显示
return false;
}
// === 只检查自己发送的消息是否重复(回环消息处理) ===
const thirtySecondsAgo = Date.now() - 30 * 1000;
const serverMsgTime = new Date(timeValue).getTime();
return this.messages.some((msg) => {
// 跳过本地消息的检查,因为本地消息会被服务器消息替换
if (msg.isLocalMessage) return false;
// 只检查自己发送的消息
if (msg.type !== "user" || msg.text !== messageContent) return false;
const msgTime = new Date(msg.time).getTime();
// 检查时间差是否在合理范围内30秒内且内容完全匹配
const timeDiff = Math.abs(msgTime - serverMsgTime);
const isRecent = msgTime > thirtySecondsAgo;
const isTimeClose = timeDiff < 30000; // 30秒内
if (isRecent && isTimeClose) {
// console.log("🔍 发现重复的回环消息:", {
// existingTime: msg.time,
// newTime: timeValue,
// timeDiff: timeDiff,
// content: messageContent.substring(0, 50),
// });
return true;
}
return false;
});
},
/**
* 创建消息对象
* @param {Object} data - 原始消息数据
* @returns {Object} 格式化的消息对象
*/
createMessageObject(data) {
// 统一时间处理逻辑,确保时间格式正确
let messageTime;
if (data.sendTime) {
// 如果有sendTime直接使用快速发送消息的情况
messageTime =
typeof data.sendTime === "string"
? data.sendTime
: new Date(data.sendTime).toISOString();
} else if (data.createTime) {
// 如果有createTime使用createTime服务器返回消息的情况
messageTime =
typeof data.createTime === "string"
? data.createTime
: new Date(data.createTime).toISOString();
} else {
// 兜底:使用当前时间
messageTime = new Date().toISOString();
}
return {
type: data.sendEmail === this.userEmail ? "user" : "system",
text: data.content,
isImage: data.type === 2,
imageUrl: data.type === 2 ? data.content : null, // 图片消息直接使用content作为imageUrl
time: messageTime,
id: data.id,
roomId: data.roomId,
sender: data.sendEmail,
isRead: false,
isLocalMessage: data.isLocalMessage || false, // 保留本地消息标记
};
},
/**
* 添加消息到聊天列表
* @param {Object} data - 消息数据
* @param {boolean} isSentByUser - 是否为用户主动发送
*/
addMessageToChat(data, isSentByUser = false) {
const messageObj = this.createMessageObject(data);
this.messages.push(messageObj);
this.$nextTick(() => {
if (isSentByUser || data.isNewMessage) {
// 自己发消息/图片,始终滚动到底部
this.scrollToBottom(true, "new");
setTimeout(() => this.scrollToBottom(true, "new"), 100);
this.userViewHistory = false;
} else if (!this.userViewHistory) {
// 对方新消息,只有用户没在看历史时才滚动
this.scrollToBottom(false, "new");
setTimeout(() => this.scrollToBottom(false, "new"), 100);
}
// 用户在翻历史消息时,不滚动
});
},
// 显示消息通知
showNotification(message) {
if (!("Notification" in window)) {
return;
}
// 检查通知权限
if (Notification.permission === "granted") {
this.createNotification(message);
} else if (Notification.permission !== "denied") {
// 请求权限
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
this.createNotification(message);
}
});
}
},
// 创建通知
createNotification(message) {
const notification = new Notification(
this.$t("chat.newMessage") || "新消息",
{
body: message.isImage
? `[ ${this.$t("chat.pictureMessage")}]` || "[图片消息]"
: message.text,
icon: "/path/to/notification-icon.png", // 添加适当的图标
}
);
notification.onclick = () => {
// 点击通知时打开聊天窗口
window.focus();
this.openChat(message.roomId);
};
},
// 打开聊天窗口
async openChat(roomId) {
this.isChatOpen = true;
this.isMinimized = false;
if (roomId) {
this.currentContactId = roomId;
this.messages = this.cachedMessages[roomId] || [];
this.markMessagesAsRead(roomId);
// 等待 DOM 更新后滚动到底部
await this.$nextTick();
// console.log("[SCROLL] openChat: 打开对话触发滚动");
this.scrollToBottom(true, "new");
}
},
// 打开聊天框
async toggleChat() {
// console.log("🎯 toggleChat被调用, 当前状态:", {
// isChatOpen: this.isChatOpen,
// userEmail: this.userEmail,
// connectionStatus: this.connectionStatus,
// isWebSocketConnected: this.isWebSocketConnected,
// });
const wasOpen = this.isChatOpen;
this.isChatOpen = !this.isChatOpen;
// 1. 判别身份
const userInfo = JSON.parse(localStorage.getItem("jurisdiction") || "{}");
if (
userInfo.roleKey === "customer_service" ||
userInfo.roleKey === "admin"
) {
// 客服用户 跳转到客服页面
this.userType = 2;
const lang = this.$i18n.locale;
this.$router.push(`/${lang}/customerService`);
return;
}
if (!wasOpen && this.isChatOpen) {
// 只有弹出聊天框且当前在底部时才请求已读
this.$nextTick(() => {
if (this.isAtBottom()) {
this.markMessagesAsRead();
}
});
}
if (this.isChatOpen) {
try {
// === 确保用户身份已确定,但避免重复初始化 ===
if (!this.userEmail) {
// console.log("🔧 用户身份未确定,需要初始化");
await this.determineUserType();
} else {
// console.log("✅ 用户身份已确定:", this.userEmail);
}
// === 检查是否需要建立连接 ===
if (
!this.isWebSocketConnected ||
this.connectionStatus === "disconnected" ||
this.connectionStatus === "error"
) {
// console.log("🔄 需要重新连接WebSocket");
await this.connectWebSocket(this.userEmail);
} else if (
this.connectionStatus === "connected" &&
this.isWebSocketConnected &&
this.stompClient?.connected
) {
// 如果已经连接成功,直接标记验证成功
// console.log("✅ 连接状态良好,直接标记验证成功");
this.markConnectionVerified();
} else {
// 如果状态不明确,启动验证监控
// console.log("🔍 连接状态不明确,启动验证监控");
this.startConnectionVerification();
}
// === 修复:无论消息列表是否为空,都需要正确处理滚动 ===
if (this.messages.length === 0) {
await this.loadHistoryMessages();
} else {
// 如果已有消息,确保滚动到底部
await this.$nextTick();
setTimeout(() => {
this.scrollToBottom(true, "new");
}, 100);
}
// === 新增:额外的滚动保障机制,确保多窗口场景下也能正确滚动 ===
// 无论走哪个分支,都在更长的延时后再次确保滚动到底部
setTimeout(() => {
if (this.isChatOpen && this.$refs.chatBody) {
// console.log("🔄 多窗口滚动保障:确保滚动到底部");
this.scrollToBottom(true, "new");
}
}, 300);
} catch (error) {
console.error("初始化聊天失败:", error);
// === 简化初始化失败提示 ===
// this.$message.error("连接失败,请重试");
}
} else {
// === 新增:关闭聊天时清除连接验证 ===
this.clearConnectionVerification();
}
},
minimizeChat() {
this.isChatOpen = false;
this.isMinimized = true;
},
closeChat() {
this.isChatOpen = false;
this.isMinimized = true;
// this.disconnectWebSocket(); // 关闭 WebSocket 连接
},
// 添加系统消息
addSystemMessage(text) {
this.messages.push({
type: "system",
text: text,
isImage: false,
time: new Date().toISOString(),
});
// 不做任何滚动
},
// 自动回复 (仅在无法连接服务器时使用)
handleAutoResponse(message) {
setTimeout(() => {
let response =
this.$t("chat.beSorry") ||
"抱歉,我暂时无法回答这个问题。请排队等待人工客服或提交工单。";
// 检查是否匹配自动回复关键词
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().toISOString(),
});
if (!this.isChatOpen) {
this.unreadMessages++;
}
// 不做任何滚动
}, 1000);
},
// === 移除滚动自动加载功能,简化逻辑 ===
// handleChatScroll() {
// // 不再自动加载历史消息,只有用户主动点击才加载
// },
// <!-- 新增加载完成事件 -->
handleImageLoad(msg) {
if (msg && msg.isHistory) {
// console.log("[SCROLL] handleImageLoad: 历史消息图片加载,不滚动");
return;
}
// console.log("[SCROLL] handleImageLoad: 新消息图片加载触发滚动");
this.scrollToBottom(true, "new");
},
//滚动到底部
scrollToBottom(force = false, reason = "new") {
if (!this.$refs.chatBody) {
console.warn("[DEBUG] scrollToBottom: chatBody不存在");
return;
}
const chatBody = this.$refs.chatBody;
const before = chatBody.scrollTop;
const scrollHeight = chatBody.scrollHeight;
const clientHeight = chatBody.clientHeight;
// console.log(
// `[DEBUG] scrollToBottom called. force=${force}, reason=${reason}, before=${before}, scrollHeight=${scrollHeight}, clientHeight=${clientHeight}`
// );
const performScroll = () => {
chatBody.scrollTop = chatBody.scrollHeight;
// console.log(`[DEBUG] performScroll: after=${chatBody.scrollTop}`);
};
this.$nextTick(() => {
this.$nextTick(() => {
performScroll();
if (force) {
setTimeout(() => {
if (this.$refs.chatBody) {
performScroll();
}
}, 50);
}
});
});
},
formatTime(date) {
if (!date) return "";
try {
let timeStr = "";
if (typeof date === "string") {
// 后端返回的时间字符串,格式如 "2025-06-11T03:10:09"
timeStr = date;
} else if (date instanceof Date) {
// 本地时间对象转换为ISO字符串
timeStr = date.toISOString();
} else {
return String(date);
}
// 处理后端时间直接去掉T提取年月日时分
if (timeStr.includes("T")) {
// 分离日期和时间部分
const [datePart, timePart] = timeStr.split("T");
if (datePart && timePart) {
// 提取时分(去掉秒和毫秒)
const timeOnly = timePart.split(":").slice(0, 2).join(":");
// === 使用与客服页面完全相同的时间判断逻辑 ===
const now = new Date();
const nowUTC = now.toISOString().split("T")[0]; // 当前UTC日期
const msgUTC = datePart; // 消息日期部分
if (nowUTC === msgUTC) {
return `UTC ${this.$t("chat.today")} ${timeOnly}`;
}
// 判断昨天
const yesterdayUTC = new Date(Date.now() - 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];
if (yesterdayUTC === msgUTC) {
return `UTC ${this.$t("chat.yesterday")} ${timeOnly}`;
}
return `UTC ${datePart} ${timeOnly} `;
}
}
// 兜底处理使用Date对象解析
const dateObj = new Date(timeStr);
if (isNaN(dateObj.getTime())) {
return timeStr; // 如果解析失败,返回原始字符串
}
const year = dateObj.getUTCFullYear();
const month = String(dateObj.getUTCMonth() + 1).padStart(2, "0");
const day = String(dateObj.getUTCDate()).padStart(2, "0");
const hours = String(dateObj.getUTCHours()).padStart(2, "0");
const minutes = String(dateObj.getUTCMinutes()).padStart(2, "0");
return `UTC ${year}-${month}-${day} ${hours}:${minutes} `;
} catch (error) {
console.error("格式化时间失败:", error);
return String(date);
}
},
// 聊天分割条时间格式化
/**
* 聊天分割条时间格式化
* @param {string|Date} date
* @returns {string}
*/
formatTimeDivider(date) {
if (!date) return "";
try {
let timeStr = "";
if (typeof date === "string") {
// 后端返回的时间字符串,格式如 "2025-06-11T03:10:09"
timeStr = date;
} else if (date instanceof Date) {
// 本地时间对象转换为ISO字符串
timeStr = date.toISOString();
} else {
return String(date);
}
// 处理后端时间直接去掉T提取年月日时分
if (timeStr.includes("T")) {
// 分离日期和时间部分
const [datePart, timePart] = timeStr.split("T");
if (datePart && timePart) {
// 提取时分(去掉秒和毫秒)
const timeOnly = timePart.split(":").slice(0, 2).join(":");
// === 使用与客服页面完全相同的时间判断逻辑 ===
const now = new Date();
const nowUTC = now.toISOString().split("T")[0]; // 当前UTC日期
const msgUTC = datePart; // 消息日期部分
if (nowUTC === msgUTC) {
return `UTC ${this.$t("chat.today")} ${timeOnly}`;
}
// 判断昨天
const yesterdayUTC = new Date(Date.now() - 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];
if (yesterdayUTC === msgUTC) {
return `UTC ${this.$t("chat.yesterday")} ${timeOnly}`;
}
return `UTC ${datePart} ${timeOnly} `;
}
}
// 兜底处理使用Date对象解析
const dateObj = new Date(timeStr);
if (isNaN(dateObj.getTime())) {
return timeStr; // 如果解析失败,返回原始字符串
}
const y = dateObj.getUTCFullYear();
const m = String(dateObj.getUTCMonth() + 1).padStart(2, "0");
const day = String(dateObj.getUTCDate()).padStart(2, "0");
const hour = String(dateObj.getUTCHours()).padStart(2, "0");
const min = String(dateObj.getUTCMinutes()).padStart(2, "0");
return `UTC ${y}-${m}-${day} ${hour}:${min} `;
} catch (error) {
console.error("格式化分割条时间失败:", error);
return String(date);
}
},
handleClickOutside(event) {
if (this.isChatOpen) {
const chatElement = this.$el.querySelector(".chat-dialog");
const chatIcon = this.$el.querySelector(".chat-icon");
const historyIndicator = this.$el.querySelector(".history-indicator");
// 如果点击的是历史消息加载指示器,不关闭对话框
if (historyIndicator && historyIndicator.contains(event.target)) {
return;
}
if (
chatElement &&
!chatElement.contains(event.target) &&
!chatIcon.contains(event.target)
) {
this.isChatOpen = false;
}
}
},
// 处理图片上传
async handleImageUpload(event) {
if (this.connectionStatus !== "connected") {
// console.log("当前连接状态:", this.connectionStatus);
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;
}
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
this.$message({
message: this.$t("chat.imageTooLarge") || "图片大小不能超过5MB!",
type: "warning",
});
return;
}
try {
// === 移除上传提示:图片上传通常很快,不需要提示 ===
// console.log("📤 正在上传图片...");
// 创建 FormData
const formData = new FormData();
formData.append("file", file);
// 上传图片
const response = await this.$axios({
method: "post",
url: `${process.env.VUE_APP_BASE_API}pool/ticket/uploadFile`,
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
if (response.data.code === 200) {
const imageUrl = response.data.data.url;
// 发送图片消息
this.sendImageMessage(imageUrl);
} else {
throw new Error(
response.data.msg ||
this.$t("chat.pictureFailed") ||
"发送图片失败,请重试"
);
}
} catch (error) {
console.error("图片处理失败:", error);
// === 简化错误提示 ===
this.$message.error("图片处理失败,请重试");
} finally {
this.$refs.imageUpload.value = "";
}
},
// 发送图片消息
sendImageMessage(imageUrl) {
if (!this.stompClient || !this.stompClient.connected) {
// console.log("发送图片时连接已断开,尝试重连...");
// === 移除重连提示:会自动重连,不需要打扰用户 ===
this.handleDisconnect();
return;
}
// === 立即本地显示机制 ===
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
const localMessageId = `local_img_${timestamp}_${random}`;
// 创建本地图片消息对象
const localMessage = {
id: localMessageId,
content: imageUrl,
type: 2,
sendEmail: this.userEmail,
sendTime: new Date().toISOString(),
roomId: this.roomId,
isLocalMessage: true, // 标记为本地消息
};
// 立即添加到本地聊天记录
// console.log("📤 立即显示本地图片消息:", localMessage);
this.addMessageToChat(localMessage, true);
try {
const message = {
type: 2, // 2 表示图片消息
email: this.receivingEmail,
receiveUserType: 2,
roomId: this.roomId,
content: imageUrl, // 使用接口返回的url
};
this.stompClient.send(
"/point/send/message/to/customer",
{},
JSON.stringify(message)
);
} catch (error) {
console.error("发送图片消息失败:", error);
// === 简化图片发送失败提示 ===
// this.$message.error("图片发送失败,请重试");
}
},
// 预览图片
previewImage(imageUrl) {
this.previewImageUrl = imageUrl;
this.showImagePreview = true;
},
// 关闭图片预览
closeImagePreview() {
this.showImagePreview = false;
this.previewImageUrl = "";
},
/**
* 优化:重试连接按钮处理 - 保持对话框打开,直接重连
*/
async handleRetryConnect() {
try {
// console.log("🔄 用户点击重试连接...");
// === 多窗口切换:抢占活跃权 ===
this.setWindowActive();
// === 重置连接状态,立即显示连接中 ===
this.connectionStatus = "connecting";
this.connectionError = null;
this.showRefreshButton = false;
this.isReconnecting = false; // 不标记为自动重连
this.isConnectionVerified = false;
// === 重置错误处理状态 ===
this.isHandlingError = false;
this.lastErrorTime = 0;
this.reconnectAttempts = 0;
// 清除连接验证定时器
this.clearConnectionVerification();
// === 强制断开旧连接 ===
// console.log("⚡ 强制断开旧连接...");
await this.forceDisconnectAll();
// 等待断开完成
await new Promise((resolve) => setTimeout(resolve, 500));
// === 确保用户身份已确定 ===
if (!this.userEmail) {
// console.log("🔍 重新初始化用户身份...");
await this.determineUserType();
}
// === 重新连接 WebSocket ===
// console.log("🌐 开始重新连接 WebSocket...");
await this.connectWebSocket(this.userEmail);
// 连接成功后的处理
if (this.connectionStatus === "connected") {
// console.log("✅ 重试连接成功");
// 如果消息列表为空,加载历史消息
if (this.messages.length === 0) {
await this.loadHistoryMessages();
}
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom(true);
});
}
} catch (error) {
console.error("❌ 重试连接失败:", error);
this.connectionStatus = "error";
this.isReconnecting = false;
// === 优化:只在严重错误时显示刷新按钮,一般连接失败只显示重试 ===
if (
error.message &&
(error.message.includes("handshake") ||
error.message.includes("503") ||
error.message.includes("网络"))
) {
this.connectionError =
this.$t("chat.networkAnomaly") || "网络连接异常,请稍后重试";
this.showRefreshButton = false; // 网络问题不显示刷新按钮
} else {
this.connectionError = this.$t("chat.abnormal") || "连接异常,请重试";
this.showRefreshButton =
error.message && error.message.includes("1020"); // 只有1020错误才显示刷新按钮
}
}
},
// 添加刷新页面方法
refreshPage() {
window.location.reload();
},
// 添加心跳检测方法
startHeartbeat() {
this.stopHeartbeat();
this.lastHeartbeatTime = Date.now();
this.heartbeatInterval = setInterval(() => {
const now = Date.now();
if (now - this.lastHeartbeatTime > this.heartbeatTimeout) {
// console.log("心跳超时,检查连接状态...");
// 只有在确实连接状态为已连接时才重连
if (
this.connectionStatus === "connected" &&
this.stompClient &&
this.stompClient.connected
) {
// 再次确认连接状态
if (this.stompClient.ws.readyState === WebSocket.OPEN) {
// console.log("WebSocket 连接仍然活跃,更新心跳时间");
this.lastHeartbeatTime = now;
return;
}
// console.log("连接状态异常,准备重连...");
this.handleDisconnect();
}
}
}, this.heartbeatCheckInterval);
},
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
},
handleLoginClick() {
this.isChatOpen = false;
const currentLang = this.$i18n.locale;
this.$router.push(`/${currentLang}/login`);
},
/**
* 解析Socket错误信息提取错误码
* @param {string|Error} error - 错误信息
* @returns {Object} 包含错误码和消息的对象
*/
parseSocketError(error) {
let errorMessage = "";
if (typeof error === "string") {
errorMessage = error;
} else if (error && error.message) {
errorMessage = error.message;
} else if (error && error.body) {
errorMessage = error.body;
}
// console.log("🔍 parseSocketError 输入:", errorMessage);
// === 新增:处理多种错误格式 ===
// 1. 处理 "ERROR message:1020连接数上限" 格式
if (errorMessage.includes("ERROR message:")) {
const afterMessage = errorMessage.split("ERROR message:")[1];
// console.log("🔍 发现ERROR message格式提取:", afterMessage);
// 检查是否是 "1020连接数上限" 这种格式
if (afterMessage && afterMessage.match(/^\d+/)) {
const match = afterMessage.match(/^(\d+)(.*)$/);
if (match) {
const code = match[1];
const message = match[2] || "";
// console.log(
// "🔍 解析ERROR message格式码:",
// code,
// "消息:",
// message
// );
return { code, message: message.trim(), original: errorMessage };
}
}
}
// 2. 处理逗号分隔的错误格式:如 "1020,本机连接已达上限,请先关闭已有链接"
if (errorMessage.includes(",")) {
const parts = errorMessage.split(",");
if (parts.length >= 2) {
const code = parts[0].trim();
const message = parts.slice(1).join(",").trim();
// console.log("🔍 解析逗号分隔格式,码:", code, "消息:", message);
return { code, message, original: errorMessage };
}
}
// 3. 处理纯数字开头的格式:如 "1020连接数上限"
const numMatch = errorMessage.match(/^(\d+)(.*)$/);
if (numMatch) {
const code = numMatch[1];
const message = numMatch[2].trim();
// console.log("🔍 解析数字开头格式,码:", code, "消息:", message);
return { code, message, original: errorMessage };
}
// console.log("🔍 未匹配到任何格式,返回原始消息");
return { code: null, message: errorMessage, original: errorMessage };
},
/**
* 检查是否是连接数上限错误
* @param {string} errorMessage - 错误消息
* @returns {boolean} 是否是连接数上限错误
*/
isConnectionLimitError(errorMessage) {
// console.log("🔍 检查是否为连接数上限错误,输入:", errorMessage);
// console.log("🔍 错误信息类型:", typeof errorMessage);
if (!errorMessage) {
// console.log("🔍 错误信息为空返回false");
return false;
}
// === 确保错误信息是字符串 ===
const errorStr = String(errorMessage);
// console.log("🔍 转换为字符串后:", errorStr);
// === 新增:使用解析方法检查错误码 ===
const { code, message } = this.parseSocketError(errorStr);
// console.log("🔍 解析后的错误码:", code, "消息:", message);
// 检查错误码
if (code === "1020") {
// console.log("✅ 发现1020错误码");
return true;
}
// 检查错误消息内容
const lowerMessage = message.toLowerCase();
// console.log("🔍 小写后的消息:", lowerMessage);
const isLimitError =
lowerMessage.includes("连接数已达上限") ||
lowerMessage.includes("本机连接数已达上限") ||
lowerMessage.includes("本机连接已达上限") ||
lowerMessage.includes("无法连接到已上线") ||
lowerMessage.includes("请先关闭已有链接") ||
lowerMessage.includes("maximum connections") ||
lowerMessage.includes("connection limit") ||
lowerMessage.includes("too many connections") ||
lowerMessage.includes("1020"); // 错误码1020
// console.log("🔍 连接数上限错误检查结果:", isLimitError);
// === 兜底检查多种方式检测1020错误 ===
if (!isLimitError) {
if (errorStr.includes("1020")) {
// console.log("🔍 兜底检查1发现1020字符串");
return true;
}
if (errorStr.includes("ERROR message:1020")) {
// console.log("🔍 兜底检查2发现ERROR message:1020");
return true;
}
if (
errorStr.includes("连接数上限") ||
errorStr.includes("连接数已达上限")
) {
// console.log("🔍 兜底检查3发现连接数上限关键词");
return true;
}
}
return isLimitError;
},
/**
* 处理连接数上限错误的特殊逻辑
* === 多连接优化后端支持最多10个连接简化错误处理 ===
*/
async handleConnectionLimitError() {
// console.log("🚫 检测到连接数上限错误超过10个连接");
// === 立即设置错误状态 ===
this.connectionStatus = "error";
this.connectionError =
this.$t("chat.connectionLimitError") ||
"连接数已达上限超过10个窗口请关闭一些窗口后重试";
this.isWebSocketConnected = false;
this.isReconnecting = false;
this.showRefreshButton = false; // 不显示刷新按钮,用户只需关闭多余窗口
// === 确保聊天对话框保持打开状态 ===
this.isChatOpen = true;
this.isMinimized = false;
this.$forceUpdate();
// console.log("🔥 连接数上限错误处理完成,提示用户关闭多余窗口");
},
/**
* 强制断开所有连接
*/
async forceDisconnectAll() {
// console.log("强制断开所有现有连接...");
// 停止心跳
this.stopHeartbeat();
// 清除所有定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.activityCheckInterval) {
clearInterval(this.activityCheckInterval);
this.activityCheckInterval = null;
}
// === 新增:清除连接验证定时器 ===
this.clearConnectionVerification();
// 断开当前STOMP连接
if (this.stompClient) {
try {
// 取消所有订阅
if (this.stompClient.subscriptions) {
Object.keys(this.stompClient.subscriptions).forEach((id) => {
try {
this.stompClient.unsubscribe(id);
} catch (error) {
// console.warn("取消订阅失败:", error);
}
});
}
// 强制断开连接
if (this.stompClient.connected) {
this.stompClient.disconnect();
}
// 如果有底层WebSocket直接关闭
if (
this.stompClient.ws &&
this.stompClient.ws.readyState === WebSocket.OPEN
) {
this.stompClient.ws.close();
}
} catch (error) {
// console.warn("断开STOMP连接时出错:", error);
}
// 清空stompClient引用
this.stompClient = null;
}
// 重置连接状态
this.isWebSocketConnected = false;
this.reconnectAttempts = 0;
this.connectionError = null;
// console.log("所有连接已强制断开");
},
/**
* 统一处理Socket连接错误支持多语言和后端错误码
* @param {Object|string} errorInput - 错误信息
* @returns {boolean} 是否已处理true=已处理,不再重连)
*/
handleConnectionError(errorInput) {
if (!errorInput) return false;
let parsedError;
if (typeof errorInput === "string") {
parsedError = this.parseSocketError(errorInput);
} else if (errorInput.message) {
parsedError = this.parseSocketError(errorInput.message);
} else {
parsedError = this.parseSocketError(errorInput);
}
// console.log("🔍 解析的错误信息:", parsedError);
// 获取错误码(字符串格式)
const errorCode = parsedError.code;
const errorMessage = parsedError.message || parsedError.original;
// 根据错误码处理不同情况
switch (errorCode) {
case "1020": // IP_LIMIT_CONNECT
// console.log("🚫 处理1020错误连接数上限");
// 连接数上限错误已由 handleConnectionLimitError 专门处理
return false;
case "1021": // MAX_LIMIT_CONNECT
// console.log("🚫 处理1021错误服务器连接数上限");
this.connectionError =
this.$t("chat.serverBusy") || "服务器繁忙,请稍后刷新重试";
this.connectionStatus = "error";
this.isReconnecting = false;
this.showRefreshButton = true;
this.$message.error(this.connectionError);
return true;
case "1022": // SET_PRINCIPAL_FAIL
// console.log("🚫 处理1022错误身份设置失败");
this.connectionError =
this.$t("chat.identityError") || "身份验证失败,请刷新页面重试";
this.connectionStatus = "error";
this.isReconnecting = false;
this.showRefreshButton = true;
this.$message.error(this.connectionError);
return true;
case "1023": // GET_PRINCIPAL_FAIL
// console.log("🚫 处理1023错误用户信息获取失败");
this.connectionError =
this.$t("chat.emailError") || "用户信息获取失败,请刷新页面重试";
this.connectionStatus = "error";
this.isReconnecting = false;
this.showRefreshButton = true;
this.$message.error(this.connectionError);
return true;
default:
// console.log("🔄 未知错误码或无错误码,使用默认处理");
return false;
}
},
// === 新增:连接验证相关方法 ===
/**
* 启动连接验证机制
* 在连接建立后的1分钟内验证连接是否真正可用
* 通过订阅成功和接收任何消息来验证不主动发送ping消息
* 无论是否在connecting状态只要1分钟没有验证成功都强制重连
*/
startConnectionVerification() {
// console.log("🔍 启动连接验证机制(被动验证)...");
// console.log("当前连接状态:", this.connectionStatus);
// console.log("当前WebSocket连接状态:", this.isWebSocketConnected);
// console.log("当前STOMP连接状态:", this.stompClient?.connected);
this.isConnectionVerified = false;
// 清除之前的验证定时器
this.clearConnectionVerification();
// 如果已经是connected状态且STOMP也连接成功检查是否可以立即验证
if (
this.connectionStatus === "connected" &&
this.isWebSocketConnected &&
this.stompClient?.connected
) {
// console.log("✅ 连接状态良好,立即标记为已验证");
this.markConnectionVerified();
return;
}
// 设置1分钟验证超时 - 无论在什么状态下
this.connectionVerifyTimer = setTimeout(() => {
if (!this.isConnectionVerified) {
// console.log(
// "⏰ 连接验证超时1分钟当前状态:",
// this.connectionStatus
// );
// console.log("WebSocket连接状态:", this.isWebSocketConnected);
// console.log("STOMP连接状态:", this.stompClient?.connected);
// console.log("强制断开重连");
// 无论当前状态如何,都强制重连
if (
this.connectionStatus === "connecting" ||
this.connectionStatus === "connected"
) {
this.handleConnectionTimeout();
} else {
this.handleConnectionVerificationFailure();
}
}
}, 60000); // 60秒超时
// console.log("⏲️ 已设置1分钟验证超时定时器");
// 注意采用被动验证方式不发送ping消息避免在对话框中显示验证消息
},
/**
* 标记连接已验证
*/
markConnectionVerified() {
if (!this.isConnectionVerified) {
// console.log("🎉 连接验证成功!清除所有定时器并确保状态正确");
this.isConnectionVerified = true;
this.clearConnectionVerification();
// === 重要:清除所有可能影响连接状态的定时器和状态 ===
this.isHandlingError = false;
this.isReconnecting = false;
this.reconnectAttempts = 0;
this.connectionError = null;
// 确保连接状态是正确的
this.connectionStatus = "connected";
this.isWebSocketConnected = true;
// console.log("✅ 连接验证完成,当前状态:", {
// connectionStatus: this.connectionStatus,
// isWebSocketConnected: this.isWebSocketConnected,
// isConnectionVerified: this.isConnectionVerified,
// reconnectAttempts: this.reconnectAttempts,
// isHandlingError: this.isHandlingError,
// });
// === 强制Vue重新渲染确保界面更新 ===
this.$forceUpdate();
} else {
// console.log("🔄 连接已经验证过了,跳过重复验证");
}
},
/**
* 设置调试模式,用于快速排查状态问题
*/
setupDebugMode() {
// 按 Ctrl+Shift+D 快速查看连接状态
document.addEventListener("keydown", (event) => {
if (event.ctrlKey && event.shiftKey && event.key === "D") {
this.debugConnectionStatus();
}
});
},
/**
* 调试连接状态
*/
debugConnectionStatus() {
// console.log("🔍 === 连接状态调试信息 ===");
// console.log("connectionStatus:", this.connectionStatus);
// console.log("isWebSocketConnected:", this.isWebSocketConnected);
// console.log("isConnectionVerified:", this.isConnectionVerified);
// console.log("isReconnecting:", this.isReconnecting);
// console.log("isHandlingError:", this.isHandlingError);
// console.log("reconnectAttempts:", this.reconnectAttempts);
// console.log("maxReconnectAttempts:", this.maxReconnectAttempts);
// console.log("connectionError:", this.connectionError);
// console.log("userEmail:", this.userEmail);
// console.log("lastConnectedEmail:", this.lastConnectedEmail);
// console.log("roomId:", this.roomId);
// console.log("STOMP connected:", this.stompClient?.connected);
// console.log("connectionVerifyTimer:", !!this.connectionVerifyTimer);
// console.log("reconnectTimer:", !!this.reconnectTimer);
// console.log("activityCheckInterval:", !!this.activityCheckInterval);
// console.log("heartbeatInterval:", !!this.heartbeatInterval);
// console.log("showRefreshButton:", this.showRefreshButton);
// console.log("isChatOpen:", this.isChatOpen);
// console.log("isMinimized:", this.isMinimized);
// 强制检查状态一致性
if (this.connectionStatus === "connecting" && this.isConnectionVerified) {
console.warn("⚠️ 状态不一致:连接中但已验证");
this.connectionStatus = "connected";
this.$forceUpdate();
}
if (this.connectionStatus === "connected" && !this.isWebSocketConnected) {
console.warn("⚠️ 状态不一致已连接但WebSocket未连接");
this.connectionStatus = "connecting";
this.$forceUpdate();
}
// console.log("🔍 === 调试信息结束 ===");
},
/**
* 清除连接验证定时器
*/
clearConnectionVerification() {
if (this.connectionVerifyTimer) {
// console.log("🧹 清除连接验证定时器");
clearTimeout(this.connectionVerifyTimer);
this.connectionVerifyTimer = null;
} else {
// console.log("🔍 没有需要清除的验证定时器");
}
},
/**
* 处理连接验证失败
*/
handleConnectionVerificationFailure() {
// console.log("⚠️ 连接验证失败,连接可能无法正常收发消息");
// 防止重复处理
const now = Date.now();
if (this.isHandlingError && now - this.lastErrorTime < 5000) {
// console.log("正在处理错误中,跳过重复处理");
return;
}
this.isHandlingError = true;
this.lastErrorTime = now;
// === 确保聊天对话框保持打开状态 ===
this.isChatOpen = true;
this.isMinimized = false;
// 清除验证定时器
this.clearConnectionVerification();
// 重置连接状态
this.isWebSocketConnected = false;
this.connectionStatus = "connecting"; // 改为connecting而不是error
this.connectionError = this.$t("chat.reconnecting") || "正在重新连接...";
// 2秒后重新连接
setTimeout(() => {
// console.log("🔄 连接验证失败,开始重新连接...");
this.isHandlingError = false;
// 断开当前连接
if (this.stompClient) {
try {
this.stompClient.disconnect();
} catch (error) {
// console.warn("断开连接时出错:", error);
}
}
// 重新连接
this.connectWebSocket(this.userEmail).catch((error) => {
// console.error("❌ 重新连接失败:", error);
this.isHandlingError = false;
// === 连接失败时确保对话框仍然打开 ===
this.isChatOpen = true;
this.isMinimized = false;
this.connectionStatus = "error";
this.showRefreshButton = true;
});
}, 2000);
},
/**
* 处理连接超时
*/
handleConnectionTimeout() {
// console.log("⏰ 连接超时,开始处理超时重连");
// 防止重复处理
const now = Date.now();
if (this.isHandlingError && now - this.lastErrorTime < 5000) {
// console.log("⚠️ 正在处理连接超时中,跳过重复处理");
return;
}
this.isHandlingError = true;
this.lastErrorTime = now;
// === 确保聊天对话框保持打开状态 ===
this.isChatOpen = true;
this.isMinimized = false;
// === 增加重连次数并检查限制 ===
this.reconnectAttempts++;
// console.log(
// `🔄 连接超时重连计数: ${this.reconnectAttempts}/${this.maxReconnectAttempts}`
// );
// 如果达到最大重连次数,直接设置错误状态
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log("❌ 连接超时且已达最大重连次数,停止重连");
this.isHandlingError = false;
this.connectionStatus = "error";
this.connectionError = "连接超时,请刷新页面重试";
this.showRefreshButton = true;
return;
}
// 清除连接验证定时器
this.clearConnectionVerification();
// 强制断开当前连接
this.forceDisconnectAll()
.then(() => {
// 设置connecting状态而不是error状态
this.connectionStatus = "connecting";
this.connectionError =
this.$t("chat.connectionTimedOut") || "连接超时,稍后重试...";
// 2秒后重新连接
setTimeout(() => {
// console.log("🔄 连接超时处理完成,开始重新连接...");
this.isHandlingError = false;
this.connectWebSocket(this.userEmail).catch((error) => {
console.error("❌ 超时重连失败:", error);
this.isHandlingError = false;
// === 重连失败时确保对话框仍然打开 ===
this.isChatOpen = true;
this.isMinimized = false;
this.connectionStatus = "error";
this.connectionError =
this.$t("chat.reconnectFailed") || "重连失败,请稍后重试";
// 如果已达最大重连次数,显示刷新按钮
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.showRefreshButton = true;
this.connectionError =
this.$t("chat.connectionFailed") ||
"连接失败,请刷新页面重试";
}
});
}, 2000);
})
.catch((error) => {
console.error("❌ 强制断开连接失败:", error);
this.isHandlingError = false;
// === 处理失败时确保对话框仍然打开 ===
this.isChatOpen = true;
this.isMinimized = false;
this.connectionStatus = "error";
this.connectionError =
this.$t("chat.connectionFailed") || "连接处理失败,请稍后重试";
// 如果已达最大重连次数,显示刷新按钮
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.showRefreshButton = true;
this.connectionError =
this.$t("chat.connectionFailed") || "连接失败,请刷新页面重试";
}
});
},
/**
* 检查是否是握手错误
*/
isHandshakeError(error) {
if (!error || !error.message) return false;
const message = error.message.toLowerCase();
return (
// WebSocket握手相关错误
message.includes("handshake") ||
message.includes("websocket") ||
// HTTP状态码错误WebSocket升级失败
message.includes("unexpected response code: 200") ||
message.includes("unexpected response code: 404") ||
message.includes("unexpected response code: 500") ||
message.includes("unexpected response code: 502") ||
message.includes("unexpected response code: 503") ||
// 连接被拒绝错误
message.includes("connection refused") ||
message.includes("connection denied") ||
message.includes("connection reset") ||
// 网络相关错误
message.includes("network error") ||
message.includes("connection failed") ||
// 协议升级失败
message.includes("upgrade required") ||
message.includes("bad handshake")
);
},
/**
* 处理握手错误
*/
handleHandshakeError(error) {
// console.log("🤝 检测到握手错误:", error.message);
// 防止重复处理
const now = Date.now();
if (this.isHandlingError && now - this.lastErrorTime < 5000) {
// console.log("⚠️ 正在处理握手错误中,跳过重复处理");
return;
}
this.isHandlingError = true;
this.lastErrorTime = now;
// === 立即设置为错误状态,避免卡在连接中 ===
this.isWebSocketConnected = false;
this.connectionStatus = "error";
this.isReconnecting = false;
// === 增强握手错误处理,针对不同错误码提供准确的错误信息 ===
if (error.message.includes("unexpected response code: 200")) {
this.connectionError =
this.$t("chat.serviceConfigurationError") ||
"服务配置异常,请稍后重试";
// console.log("🔴 WebSocket握手失败服务器返回200而非101升级响应");
} else if (error.message.includes("unexpected response code: 404")) {
this.connectionError =
this.$t("chat.serviceAddressUnavailable") ||
"服务地址不可用,请稍后重试";
// console.log("🔴 WebSocket握手失败服务地址404");
} else if (error.message.includes("unexpected response code: 500")) {
this.connectionError =
this.$t("chat.server500") || "服务器暂时不可用,请稍后重试";
// console.log("🔴 WebSocket握手失败服务器500错误");
} else if (error.message.includes("connection refused")) {
this.connectionError =
this.$t("chat.connectionFailedService") ||
"无法连接到服务器,请稍后重试";
// console.log("🔴 WebSocket握手失败连接被拒绝");
} else {
this.connectionError =
this.$t("chat.connectionFailed") || "连接失败,请稍后重试";
// console.log("🔴 WebSocket握手失败", error.message);
}
// === 增加重连次数并检查限制 ===
this.reconnectAttempts++;
// console.log(
// `🔄 握手错误重连计数: ${this.reconnectAttempts}/${this.maxReconnectAttempts}`
// );
// 如果达到最大重连次数,不再重试
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
// console.log("❌ 握手错误重连次数已达上限,停止重连");
this.isHandlingError = false;
this.showRefreshButton = true;
this.connectionError =
this.$t("chat.connectionFailed") || "连接失败,请刷新页面重试";
return;
}
// 等待3秒后重试连接
setTimeout(() => {
// console.log("🔄 握手错误处理完成,开始重新连接...");
this.isHandlingError = false;
// 设置为连接中状态
this.connectionStatus = "connecting";
this.connectWebSocket(this.userEmail).catch((retryError) => {
// console.error("❌ 握手错误重连失败:", retryError);
this.isHandlingError = false;
this.connectionStatus = "error";
this.connectionError =
this.$t("chat.reconnectFailed") || "重连失败,请稍后重试";
});
}, 3000);
},
/**
* 处理WebSocket错误
*/
handleWebSocketError(error) {
// console.log("WebSocket级别错误:", error);
// 防止重复处理
const now = Date.now();
if (this.isHandlingError && now - this.lastErrorTime < 3000) {
// console.log("正在处理错误中,跳过重复处理");
return;
}
this.isHandlingError = true;
this.lastErrorTime = now;
// === 关键修复:立即显示错误状态和重试按钮 ===
this.isWebSocketConnected = false;
this.connectionStatus = "error";
this.connectionError =
this.$t("chat.connectionFailedCustomer") ||
"连接客服系统失败,请检查网络或稍后重试";
this.showRefreshButton = false;
// 1秒后可自动重连不影响UI
setTimeout(() => {
this.isHandlingError = false;
this.handleDisconnect();
}, 1000);
},
/**
* 处理WebSocket关闭
*/
handleWebSocketClose(event) {
// console.log("WebSocket连接关闭:", event.code, event.reason);
// 如果不是正常关闭,处理异常关闭
if (event.code !== 1000) {
this.isWebSocketConnected = false;
// === 关键修复:立即显示错误状态和重试按钮 ===
this.connectionStatus = "error";
this.connectionError =
this.$t("chat.connectionFailedCustomer") ||
"连接客服系统失败,请检查网络或稍后重试";
this.showRefreshButton = false;
// 延迟重连
setTimeout(() => {
if (!this.isWebSocketConnected) {
this.handleDisconnect();
}
}, 1000);
}
},
// 处理退出登录
handleLogout() {
// 断开 WebSocket 连接
this.disconnectWebSocket();
// 重置状态
this.isChatOpen = false;
this.isMinimized = true;
this.messages = [];
this.updateUnreadMessages(0); // === 使用localStorage管理 ===
this.connectionStatus = "disconnected";
this.isWebSocketConnected = false;
this.userType = 0;
this.userEmail = "";
this.roomId = "";
// === 清除游客身份缓存,为下次访问做准备 ===
sessionStorage.removeItem("chatGuestEmail");
this.lastConnectedEmail = null;
// console.log("🧹 退出登录时清除游客身份缓存");
},
// 处理登录成功
async handleLoginSuccess() {
// 断开原有连接
this.disconnectWebSocket();
// === 清除游客身份缓存,确保使用登录用户身份 ===
sessionStorage.removeItem("chatGuestEmail");
this.lastConnectedEmail = null;
// console.log("🧹 登录成功时清除游客身份缓存");
// 等待一小段时间,确保 localStorage 已更新
await new Promise((resolve) => setTimeout(resolve, 100));
// 重试机制:最多重试 3 次
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
// 重新判别身份
await this.determineUserType();
// console.log(this.userEmail, "userEmail 重新登录成功");
// 检查是否成功获取到 userEmail
if (this.userEmail && this.userEmail !== "") {
// 用新身份重新连接
if (!this.isWebSocketConnected) {
await this.connectWebSocket(this.userEmail);
}
// 重新加载历史消息
await this.loadHistoryMessages();
break; // 成功后跳出循环
} else {
throw new Error("未获取到有效的用户邮箱");
}
} catch (error) {
retryCount++;
console.warn(
`登录处理失败 (尝试 ${retryCount}/${maxRetries}):`,
error
);
if (retryCount < maxRetries) {
// 等待一段时间后重试
await new Promise((resolve) =>
setTimeout(resolve, 500 * retryCount)
);
} else {
// console.error("登录处理最终失败,已达到最大重试次数");
// this.$message.error("聊天功能初始化失败,请刷新页面");
}
}
}
},
/**
* 判断是否应自动滚动到底部
* - 聊天窗口已打开
* - 当前滚动条已在底部或距离底部不超过50px
* @returns {boolean}
*/
shouldAutoScrollOnNewMessage() {
if (!this.isChatOpen) return false;
const chatBody = this.$refs.chatBody;
if (!chatBody) return false;
const { scrollTop, scrollHeight, clientHeight } = chatBody;
const distanceToBottom = Math.abs(
scrollHeight - (scrollTop + clientHeight)
);
const atBottom = distanceToBottom < 100; // 阈值可根据实际体验调整
// console.log(
// "[DEBUG] scrollTop:",
// scrollTop,
// "clientHeight:",
// clientHeight,
// "scrollHeight:",
// scrollHeight,
// "distanceToBottom:",
// distanceToBottom,
// "atBottom:",
// atBottom
// );
return atBottom;
},
/**
* 判断是否在聊天框底部允许2px误差
* @returns {boolean}
*/
isAtBottom() {
const chatBody = this.$refs.chatBody;
if (!chatBody) return true;
return (
chatBody.scrollHeight - chatBody.scrollTop - chatBody.clientHeight < 2
);
},
/**
* 聊天区滚动事件处理,标记用户是否在查看历史
* 到底部时自动清零未读数,并请求已读
*/
handleChatBodyScroll() {
if (!this.$refs.chatBody) return;
if (this.isAtBottom()) {
this.userViewHistory = false;
// 到底部时自动清零未读数,并请求已读
this.markMessagesAsRead();
} else {
this.userViewHistory = true;
}
},
/**
* 聊天框初始化时多次兜底滚动到底部,保证异步内容加载后滚动到位
* 只在初始化/刷新/首次加载时调用,不影响其他功能
*/
scrollToBottomOnInit() {
let tries = 0;
const maxTries = 5;
const interval = 100;
const tryScroll = () => {
this.scrollToBottom(true, "init");
tries++;
if (tries < maxTries) {
setTimeout(tryScroll, interval);
}
};
tryScroll();
},
/**
* 格式化消息文本:将\n换行符转为<br />并转义HTML防止XSS
* @param {string} text
* @returns {string}
*/
formatMessageText(text) {
if (!text) return "";
// 转义HTML
const escapeHtml = (str) =>
str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
return escapeHtml(text).replace(/\n/g, "<br />");
},
},
beforeDestroy() {
// 移除退出登录事件监听
this.$bus.$off("user-logged-out", this.handleLogout);
// 调用退出登录处理方法
this.handleLogout();
// === 移除滚动监听(已禁用) ===
// if (this.$refs.chatBody) {
// this.$refs.chatBody.removeEventListener("scroll", this.handleChatScroll);
// }
// 移除页面可见性变化监听
document.removeEventListener(
"visibilitychange",
this.handleVisibilityChange
);
// 确保在销毁时断开连接
if (this.stompClient) {
this.stompClient.disconnect();
this.stompClient = null;
}
// 断开 WebSocket 连接
this.disconnectWebSocket();
// 清除活动检测定时器
if (this.activityCheckInterval) {
clearInterval(this.activityCheckInterval);
}
// 清除重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
// === 新增:清除连接验证定时器 ===
this.clearConnectionVerification();
// 移除新添加的事件监听
window.removeEventListener("online", this.handleNetworkChange);
window.removeEventListener("offline", this.handleNetworkChange);
document.removeEventListener("mousemove", this.updateLastActivityTime);
document.removeEventListener("keydown", this.updateLastActivityTime);
document.removeEventListener("click", this.updateLastActivityTime);
// === 移除localStorage监听器 ===
window.removeEventListener("storage", this.handleStorageChange);
this.stopHeartbeat();
this.$bus.$off("user-logged-in", this.handleLoginSuccess); //移除登录成功事件监听
if (this.$refs.chatBody) {
this.$refs.chatBody.removeEventListener(
"scroll",
this.handleChatBodyScroll
);
}
},
};
</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;
}
}
&.disconnected {
i {
color: #f39c12;
}
p {
color: #f39c12;
}
}
.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 {
position: relative;
max-width: 70%;
padding: 18px 15px 10px 15px; // 上方多留空间给时间
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
.message-time {
position: absolute;
top: 6px;
right: 15px;
font-size: 11px;
color: #bbb;
pointer-events: none;
user-select: none;
}
// 用户消息气泡内时间颜色适配
.chat-message-user & .message-time {
color: rgba(255, 255, 255, 0.7);
}
}
.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;
}
}
.system-hint {
text-align: center;
font-size: 12px;
color: #999;
margin: 10px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.history-indicator {
text-align: center;
font-size: 12px;
color: #666;
margin: 10px 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
padding: 5px;
border-radius: 15px;
background-color: #f0f0f0;
width: fit-content;
margin: 0 auto 10px;
&:hover {
background-color: #e0e0e0;
color: #333;
}
}
.chat-message-history {
opacity: 0.8;
}
.chat-message-loading,
.chat-message-hint {
margin: 5px 0;
justify-content: center;
span {
color: #999;
font-size: 12px;
}
}
.message-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
font-size: 11px;
}
.message-time {
color: #999;
}
.message-read-status {
color: #999;
font-size: 10px;
margin-left: 5px;
}
.chat-message-user .message-read-status {
color: rgba(255, 255, 255, 0.7);
}
.network-status {
position: fixed;
top: 80px;
right: 20px;
padding: 8px 16px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
background-color: #fef0f0;
color: #f56c6c;
}
.guest-notice {
background-color: #f8f9fa;
border-radius: 8px;
padding: 8px 12px;
margin-bottom: 10px;
text-align: center;
border: 1px solid #e0e0e0;
.guest-notice-content {
display: inline-flex;
gap: 2px;
color: #666;
font-size: 13px;
line-height: 1.4;
i {
color: #ac85e0;
font-size: 16px;
flex-shrink: 0;
}
.login-link {
color: #ac85e0;
text-decoration: none;
font-weight: bold;
cursor: pointer;
transition: color 0.3s;
margin: 0 2px;
&:hover {
color: #6e3edb;
text-decoration: underline;
}
}
}
}
.error-actions {
display: flex;
gap: 10px;
margin-top: 16px;
justify-content: center;
.retry-button,
.refresh-button {
padding: 8px 16px;
border: none;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
}
}
.retry-button {
background-color: #ac85e0;
color: white;
&:hover {
background-color: #6e3edb;
}
}
.refresh-button {
background-color: #f0f0f0;
color: #666;
padding: 0px 16px;
&:hover {
background-color: #e0e0e0;
}
}
}
.chat-time-divider {
text-align: center;
margin: 16px 0;
font-size: 12px;
color: #fff;
background: rgba(180, 180, 180, 0.6);
display: inline-block;
padding: 2px 12px;
border-radius: 10px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
left: 50%;
transform: translateX(-50%);
position: relative;
}
/* === 移除本地消息的视觉反馈小黄点 === */
/* .chat-message-local {
// 不再显示任何特殊样式
} */
</style>