m2pool_web_frontend/mining-pool/src/views/customerService/index.vue

4238 lines
130 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="cs-chat-container">
<!-- 添加网络状态提示 v-if="networkStatus === 'offline'" -->
<div v-if="networkStatus === 'offline'" class="network-status">
<i class="el-icon-warning"></i>
<span>{{ $t("chat.networkError") || "网络连接已断开" }}</span>
</div>
<!-- 添加连接状态提示 -->
<div
v-if="connectionStatus !== 'connected'"
class="connection-status"
:class="connectionStatus"
>
<i
:class="
connectionStatus === 'error' ? 'el-icon-warning' : 'el-icon-loading'
"
></i>
<span
>{{
connectionStatus === "error"
? $t("chat.Disconnected") || "连接已断开"
: $t("chat.reconnecting") || "正在连接..."
}}
</span>
</div>
<!-- 聊天窗口主体 -->
<div class="cs-chat-wrapper">
<!-- 左侧联系人列表 -->
<div class="cs-contact-list">
<div class="cs-header">
<i class="el-icon-s-custom"></i>
{{ $t("chat.contactList") || "联系列表" }}
</div>
<div class="cs-search">
<el-input
prefix-icon="el-icon-search"
v-model="searchText"
:placeholder="$t(`chat.search`) || '搜索最近联系人'"
clearable
></el-input>
</div>
<!-- 联系人列表 -->
<div class="cs-contacts">
<div
v-for="(contact, index) in filteredContacts"
:key="index"
class="cs-contact-item"
:class="{ active: currentContactId === contact.roomId }"
@click="selectContact(contact.roomId)"
:title="contact.name"
>
<div class="cs-avatar">
<i class="iconfont icon-icon28" style="font-size: 1.5vw"></i>
<span v-if="contact.unread" class="unread-badge">{{
contact.unread
}}</span>
<!-- 添加游客标识 -->
<span v-if="contact.isGuest" class="guest-badge">{{
$t("chat.tourist") || "游客"
}}</span>
</div>
<div class="cs-contact-info">
<div class="cs-contact-name">
{{ contact.name }}
</div>
<div class="cs-contact-msg">
<span v-if="contact.important" class="important-tag"
>[{{ $t("chat.important") || "重要" }}]
</span>
<!-- {{ contact.lastMessage }} -->
</div>
<div>
<span
class="cs-contact-time"
:title="contact.lastTime"
>{{ formatLastTime(contact.lastTime) }}</span
>
</div>
</div>
<!-- 添加重要标记图标 -->
<div
class="important-star"
:class="{ 'is-important': contact.important }"
@click.stop="toggleImportant(contact.roomId, !contact.important)"
:title="$t(`chat.markAsImportant`) || '标记为重要聊天'"
>
<i class="el-icon-star-on"></i>
</div>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="cs-chat-area">
<!-- 顶部信息栏 -->
<div class="cs-chat-header">
<div class="cs-chat-title">
{{
currentContact
? currentContact.name
: $t("chat.chooseFirst") || "请选择联系人"
}}
<el-tag
v-if="currentContact && currentContact.important"
size="small"
type="danger"
@click="
toggleImportant(
currentContact.roomId,
!currentContact.important
)
"
>
{{ $t("chat.important") || "重要" }}
</el-tag>
<el-tag
v-else-if="currentContact"
size="small"
type="info"
style="cursor: pointer;"
@click="
toggleImportant(
currentContact.roomId,
!currentContact.important
)
"
>
{{ $t("chat.markAsImportant") || "标记为重要" }}
</el-tag>
</div>
<div class="cs-header-actions">
<!-- loadHistory -->
<i
class="el-icon-time"
:title="$t(`chat.history`) || '历史记录'"
@click="loadMoreHistory"
></i>
<!-- <i
class="el-icon-refresh"
title="刷新"
@click="refreshMessages"
></i> -->
<!-- <i class="el-icon-more" title="更多选项"></i> -->
</div>
</div>
<!-- 聊天内容区域 -->
<div
class="cs-chat-messages"
ref="messageContainer"
@scroll="handleScroll"
>
<div v-if="!currentContact" class="cs-empty-chat">
<i class="el-icon-chat-dot-round"></i>
<p>{{ $t("chat.notSelected") || "您尚未选择联系人" }}</p>
</div>
<template v-else>
<!-- 历史消息加载区域 -->
<div v-if="currentMessages.length > 0" class="history-section">
<!-- 加载更多历史消息按钮 -->
<div
v-if="hasMoreHistory"
class="history-indicator"
@click="loadMoreHistory"
style="
cursor: pointer;
text-align: center;
color: #409eff;
margin-bottom: 10px;
font-size: 0.7vw;
"
>
<i class="el-icon-arrow-up"></i>
<span>{{ $t("chat.loadMore") || "加载更多历史消息" }}</span>
</div>
<!-- 没有更多历史消息提示 -->
<div
v-else
class="no-more-history"
style="
text-align: center;
color: #909399;
margin-bottom: 10px;
font-size: 0.7vw;
padding: 8px 0;
"
>
<i class="el-icon-info"></i>
<span>{{
noMoreHistoryMessage ||
$t("chat.noMoreHistory") ||
"没有更多历史消息"
}}</span>
</div>
</div>
<div v-if="messagesLoading" class="cs-loading">
<i class="el-icon-loading"></i>
<p>{{ $t("chat.loading") || "加载消息中..." }}</p>
</div>
<div v-else-if="currentMessages.length === 0" class="cs-empty-chat">
<i class="el-icon-chat-line-round"></i>
<p>{{ $t("chat.None") || "暂无消息记录" }}</p>
</div>
<div v-else class="cs-message-list">
<div
v-for="(message, idx) in currentMessages"
:key="idx"
class="cs-message"
:class="{ 'cs-message-self': message.isSelf }"
>
<div class="cs-message-time" v-if="showMessageTime(idx)">
{{ formatTime(message.time) }}
</div>
<div class="cs-message-content">
<div class="cs-avatar">
<i class="iconfont icon-icon28" style="font-size: 2vw"></i>
<!-- <el-avatar
:size="36"
:src="
message.isSelf
? getDefaultAvatar('我')
: getDefaultAvatar(message.sender)
"
>
{{ message.sender ? message.sender.charAt(0) : "?" }}
</el-avatar> -->
</div>
<div class="cs-bubble">
<div class="cs-sender">{{ message.sender }}</div>
<div
v-if="!message.isImage"
class="cs-text"
v-html="formatMessageContent(message.content)"
></div>
<div v-else class="cs-image">
<img
:src="message.content"
@click="previewImage(message.content)"
/>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- 输入区域 -->
<div class="cs-chat-input">
<div class="cs-toolbar">
<i
class="el-icon-picture-outline"
:title="$t(`chat.sendPicture`) || '发送图片'"
@click="openImageUpload"
></i>
<input
type="file"
ref="imageInput"
accept="image/*"
style="display: none"
@change="handleImageUpload"
/>
<!-- <i class="el-icon-folder-opened" title="发送文件"></i> -->
<!-- <i class="el-icon-s-opportunity" title="发送表情"></i> -->
<!-- <i class="el-icon-scissors" title="截图"></i> -->
</div>
<!-- @keydown.enter.native="handleKeyDown" -->
<div class="cs-input-area">
<el-input
type="textarea"
v-model="inputMessage"
:rows="3"
:maxlength="400"
:disabled="!currentContact"
resize="none"
:placeholder="
$t(`chat.inputMessage`) ||
`请输入消息按Enter键发送按Ctrl+Enter键换行`
"
@keydown.native="handleKeyDown"
></el-input>
</div>
<div class="cs-send-area">
<span class="cs-counter">{{ inputMessage.length }}/400</span>
<el-button
type="primary"
:disabled="!currentContact || !inputMessage.trim() || sending"
@click="sendMessage"
>
<i v-if="sending" class="el-icon-loading"></i>
<span v-else>{{ $t("chat.send") || "发送" }}</span>
</el-button>
</div>
</div>
</div>
</div>
<div
v-if="showScrollButton"
class="scroll-to-bottom"
@click="scrollToBottom(true)"
>
{{ $t("chat.bottom") || "回到底部" }} <i class="el-icon-arrow-down"></i>
</div>
<!-- 图片预览 -->
<el-dialog
:visible.sync="previewVisible"
append-to-body
class="image-preview-dialog"
>
<img
:src="previewImageUrl"
class="preview-image"
:alt="$t(`chat.Preview`) || '预览图片'"
/>
</el-dialog>
</div>
</template>
<script>
import {
getRoomList,
getHistory,
getHistory7,
getReadMessage,
getUpdateRoom,
getFileUpdate,
} from "../../api/customerService";
// 正确导入 Client
import { Client, Stomp } from "@stomp/stompjs";
export default {
name: "CustomerServiceChat",
data() {
return {
searchText: "",
inputMessage: "",
currentContactId: null,
previewVisible: false,
previewImageUrl: "",
contacts: [],
messages: {},
messagesLoading: false,
sending: false,
loadingRooms: true,
stompClient: null,
wsConnected: false,
userEmail: "", // 当前客服邮箱
userType: 1, // 0或者1 游客或者登录用户
loadingHistory: false, // 是否正在加载历史消息
userViewHistory: false, // 用户是否在浏览历史
userScrolled: false, // 新增:用户是否手动滚动过
history7Params: {
//7天历史消息参数
id: "", //最后一条消息id
roomId: "", //聊天室id
userType: 2, //用户类型
email: "", //客服邮箱
},
historyAllParams: {
//7天以前的历史消息
id: "", //最后一条消息id
roomId: "", //聊天室id
userType: 2, //用户类型
},
receiveUserType: "", //接收者类型
manualCreatedRooms: [], //手动创建的聊天室
chatRooms: [], // 初始化聊天室列表数组
isWebSocketConnected: false,
connectionStatus: "disconnected",
isLoadingMoreContacts: false, // 是否正在加载更多联系人
lastContactTime: null, // 最后一个联系人的时间
showScrollButton: false,
visibilityHandler: null, // 页面可见性处理器
reconnectTimer: null, // 重连定时器
maxReconnectAttempts: 5, // 最大重连次数
reconnectInterval: 5000, // 重连间隔(ms)
reconnectAttempts: 0, // 当前重连次数
isHandlingError: false, // 防止重复处理错误
lastErrorTime: 0, // 最后一次错误时间
lastActivityTime: Date.now(), // 最后活动时间
activityCheckInterval: null, // 活动检测定时器
activityEvents: null, // 活动监听事件列表
activityHandler: null, // 活动监听处理函数
connectionVerifyTimer: null, // 连接验证定时器
connectionVerifyTimeout: 60000, // 连接验证超时时间(60秒)
isConnectionVerified: false, // 连接是否已验证
// === 新增:心跳检测相关 ===
heartbeatInterval: null, // 心跳定时器
heartbeatTimeout: 30000, // 心跳间隔30秒
lastHeartbeatTime: 0, // 最后一次心跳时间
connectionCheckInterval: null, // 连接检查定时器
connectionCheckTimeout: 60000, // 连接检查间隔60秒
// 历史消息加载状态控制
hasMoreHistory: true, // 是否还有更多历史消息
noMoreHistoryMessage: "", // 无更多历史消息时的提示文字
networkStatus: "online",
};
},
computed: {
filteredContacts() {
//搜索联系人
if (!this.searchText) {
return this.contacts;
}
return this.contacts.filter((contact) =>
contact.name.toLowerCase().includes(this.searchText.toLowerCase())
);
},
currentContact() {
//选中联系人对象
return this.contacts.find(
(contact) => contact.roomId === this.currentContactId
);
},
currentMessages() {
//当前聊天室消息
return this.messages[this.currentContactId] || [];
},
},
async created() {
try {
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
window.addEventListener("setItem", () => {
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
});
// 获取聊天室列表
await this.fetchRoomList();
// 在组件创建时加载手动创建的聊天室
this.loadManualCreatedRooms();
console.log(this.userEmail, "初始化的时候");
// 初始化 WebSocket 连接
this.initWebSocket();
} catch (error) {
console.error("初始化失败:", error);
}
},
async mounted() {
// 获取聊天室列表
await this.fetchRoomList();
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
window.addEventListener("setItem", () => {
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
});
// 添加网络状态变化监听
window.addEventListener("online", this.handleNetworkChange);
window.addEventListener("offline", this.handleNetworkChange);
// 注释:不再自动滚动,等用户选择聊天室后再滚动
// this.$nextTick(() => {
// this.scrollToBottom();
// });
// 添加滚动事件监听
this.$nextTick(() => {
if (this.$refs.messageContainer) {
this.$refs.messageContainer.addEventListener(
"scroll",
this.handleScroll
);
}
});
// 添加联系人列表滚动事件监听
this.$nextTick(() => {
const contactList = document.querySelector(".cs-contacts");
if (contactList) {
contactList.addEventListener("scroll", this.handleContactListScroll);
}
});
// 添加页面可见性监听
this.visibilityHandler = () => {
if (document.visibilityState === "visible") {
// === 优化:页面变为可见时,更新活动时间并智能检查连接状态 ===
console.log("🔍 客服页面重新可见,执行连接状态检查");
this.updateLastActivityTime();
// 立即执行一次连接状态检查
this.performConnectionCheck();
// 如果连接状态不正常,则重连
if (
this.connectionStatus !== "connected" ||
!this.isWebSocketConnected
) {
console.log("🔄 客服页面可见,检测到连接异常,开始重连");
this.checkAndReconnect();
} else {
console.log("✅ 客服页面可见,连接状态正常");
// 重新启动心跳检测(可能在页面隐藏时被暂停)
if (!this.heartbeatInterval) {
this.startHeartbeat();
}
if (!this.connectionCheckInterval) {
this.startConnectionCheck();
}
}
} else {
console.log("📱 客服页面变为隐藏状态");
// 页面隐藏时不停止心跳检测,继续保持连接
}
};
document.addEventListener("visibilitychange", this.visibilityHandler);
// 添加用户活动检测
this.startActivityCheck();
// === 新增:添加用户活动监听器 ===
this.activityEvents = [
"mousedown",
"mousemove",
"keypress",
"scroll",
"touchstart",
"click",
];
this.activityHandler = () => {
this.updateLastActivityTime();
};
// 添加活动监听器
this.activityEvents.forEach((event) => {
document.addEventListener(event, this.activityHandler, true);
});
window.addEventListener("storage", this.handleStorageChange);
},
methods: {
handleKeyDown(e) {
if (e.key === "Enter") {
if (e.ctrlKey) {
// 插入换行
const textarea = e.target;
const value = textarea.value;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
// 在光标处插入\n
textarea.value =
value.substring(0, start) + "\n" + value.substring(end);
// 移动光标到新行
textarea.selectionStart = textarea.selectionEnd = start + 1;
// 阻止默认行为(防止触发发送)
e.preventDefault();
} else {
// 阻止默认行为并发送消息
e.preventDefault();
this.sendMessage();
}
}
},
// 初始化 WebSocket 连接
initWebSocket() {
if (this.isWebSocketConnected) {
console.log("WebSocket已连接跳过初始化");
return;
}
// 防止重复初始化
if (this.stompClient && this.stompClient.state !== "DISCONNECTED") {
console.log("WebSocket正在连接中跳过初始化");
return;
}
try {
// 确保之前的连接已经清理
if (this.stompClient) {
this.forceDisconnectAll();
}
console.log("开始初始化WebSocket连接...");
const baseUrl = process.env.VUE_APP_BASE_API.replace("https", "wss");
const wsUrl = `${baseUrl}chat/ws`;
this.stompClient = Stomp.client(wsUrl);
// 配置 STOMP 客户端参数
this.stompClient.splitLargeFrames = true; // 启用大型消息帧分割
this.stompClient.maxWebSocketFrameSize = 16 * 1024 * 1024; // 设置最大帧大小为16MB
this.stompClient.maxWebSocketMessageSize = 16 * 1024 * 1024; // 设置最大消息大小为16MB
this.stompClient.webSocketFactory = () => {
const ws = new WebSocket(wsUrl);
ws.binaryType = "arraybuffer"; // 设置二进制类型为 arraybuffer
return ws;
};
// 修改调试日志的方式
this.stompClient.debug = (str) => {
// 只打印与客服相关的日志
if (
str.includes("CONNECTED") ||
str.includes("DISCONNECTED") ||
str.includes("ERROR")
) {
console.log("[客服系统]", str);
}
};
this.userType = 2; //客服
const headers = {
email: this.userEmail,
type: this.userType,
};
// 添加STOMP错误处理
this.stompClient.onStompError = (frame) => {
console.error("[客服系统] STOMP 错误:", frame);
// 处理STOMP帧错误
this.handleSocketError(frame.headers?.message || frame.body);
};
// 添加重连逻辑
this.stompClient.connect(
headers,
(frame) => {
console.log("🎉 [客服系统] WebSocket 连接成功", frame);
this.isWebSocketConnected = true;
this.connectionStatus = "connected";
this.reconnectAttempts = 0;
this.isConnectionVerified = false; // 重置验证状态
this.lastHeartbeatTime = Date.now(); // 记录连接时间作为心跳时间
console.log("🔗 开始订阅客服消息...");
// 订阅消息
this.subscribeToMessages();
this.updateLastActivityTime();
// === 启动心跳检测 ===
this.startHeartbeat();
// === 启动连接状态检查 ===
this.startConnectionCheck();
// === 注意:不在这里启动验证,而是在订阅成功后 ===
console.log("⚡ 客服连接成功,等待订阅完成后验证");
},
(error) => {
console.error("[客服系统] WebSocket 错误:", error);
// 处理特定的Socket错误
this.handleSocketError(error);
}
);
// 配置心跳
this.stompClient.heartbeat.outgoing = 20000;
this.stompClient.heartbeat.incoming = 20000;
} catch (error) {
console.error("初始化 CustomerService WebSocket 失败:", error);
this.handleDisconnect();
}
},
// // 订阅消息
subscribeToMessages() {
if (!this.stompClient || !this.isWebSocketConnected) {
console.log("STOMP客户端未连接无法订阅消息");
return;
}
try {
console.log(
"开始订阅客服消息频道:",
`/sub/queue/customer/${this.userEmail}`
);
// 修改订阅路径,使用客服特定的订阅路径
const subscription1 = this.stompClient.subscribe(
`/sub/queue/customer/${this.userEmail}`,
this.handleIncomingMessage
);
// 订阅聊天室关闭消息
const subscription2 = this.stompClient.subscribe(
`/sub/queue/close/room/${this.userEmail}`,
this.handleRoomClose
);
if (subscription1 && subscription2) {
console.log(
"✅ CustomerService 成功订阅消息频道:",
`/sub/queue/customer/${this.userEmail}`
);
console.log(
"✅ CustomerService 成功订阅关闭消息频道:",
`/sub/queue/close/room/${this.userEmail}`
);
// === 订阅成功立即标记连接验证 ===
console.log("📢 客服订阅成功,立即标记连接已验证");
this.markConnectionVerified();
// 确保连接状态正确
if (this.connectionStatus !== "connected") {
console.log("📡 修正客服连接状态为connected");
this.connectionStatus = "connected";
}
} else {
console.error("❌ 客服订阅失败返回空subscription");
// 如果订阅失败,启动验证机制等待超时重连
this.startConnectionVerification();
}
} catch (error) {
console.error("❌ CustomerService 订阅消息异常:", error);
// 如果订阅异常,启动验证机制等待超时重连
this.startConnectionVerification();
}
},
// 处理聊天室关闭的方法 删除游客
handleRoomClose(message) {
try {
// 获取需要关闭的游客邮箱
const closedUserEmail = message.body;
// 标准化处理 返回的格式 "\"guest_1748242041830_jmz4c9qx5\""
const normalize = (str) => {
if (!str) return "";
if (typeof str === "object" && "value" in str) str = str.value;
str = String(str).trim().toLowerCase();
// 去除所有首尾引号
str = str.replace(/^['"]+|['"]+$/g, "");
return str;
};
const targetEmail = normalize(closedUserEmail);
// 在联系人列表中查找对应的聊天室
const contactIndex = this.contacts.findIndex((contact) => {
const contactEmail = normalize(contact.name);
return contactEmail === targetEmail;
});
// console.log("标准化比较结果:", {
// targetEmail,
// contacts: this.contacts.map(c => normalize(c.name)),
// contactIndex
// });
if (contactIndex !== -1) {
// 如果当前选中的就是被关闭的聊天室,需要清除选中状态
if (this.currentContactId === this.contacts[contactIndex].roomId) {
this.currentContactId = null;
}
// 从联系人列表中移除
this.contacts.splice(contactIndex, 1);
// 从消息列表中移除
this.$delete(this.messages, this.contacts[contactIndex].roomId);
// 从手动创建的聊天室列表中移除
const manualRoomIndex = this.manualCreatedRooms.findIndex(
(room) => room.name === closedUserEmail
);
if (manualRoomIndex !== -1) {
this.manualCreatedRooms.splice(manualRoomIndex, 1);
this.saveManualCreatedRooms();
}
// === 减少提示:聊天室关闭只在控制台记录 ===
console.log(`聊天室 ${closedUserEmail} 已关闭`);
}
} catch (error) {
console.error("处理聊天室关闭消息失败:", error);
}
},
// 断开连接
disconnectWebSocket() {
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";
} catch (error) {
console.error("断开 CustomerService WebSocket 连接失败:", error);
}
}
},
/**
* 解析Socket错误信息提取错误码和错误消息
* @param {string|Object} errorInfo - 原始错误信息
* @returns {Object} { code: string, message: string }
*/
parseSocketError(errorInfo) {
let errorMessage = "";
// 处理不同类型的错误信息
if (typeof errorInfo === "string") {
errorMessage = errorInfo;
} else if (errorInfo && typeof errorInfo === "object") {
// 从错误对象中提取消息
errorMessage =
errorInfo.message ||
errorInfo.body ||
errorInfo.headers?.message ||
String(errorInfo);
} else {
errorMessage = String(errorInfo || "");
}
// 处理格式:"1020,本机连接数已达上限,请先关闭已有链接"
if (errorMessage.includes(",")) {
const parts = errorMessage.split(",");
const code = parts[0].trim();
const message = parts.slice(1).join(",").trim();
return { code, message };
}
// 处理其他格式,尝试从消息中提取错误码
const codeMatch = errorMessage.match(/(\d{4})/);
if (codeMatch) {
return { code: codeMatch[1], message: errorMessage };
}
return { code: "", message: errorMessage };
},
/**
* 处理Socket连接错误
* @param {Object|string} error - 错误信息
*/
async handleSocketError(error) {
// 防止重复处理错误5秒内的重复错误忽略
const now = Date.now();
if (this.isHandlingError || now - this.lastErrorTime < 5000) {
console.log("正在处理错误或错误处理间隔太短,跳过此次错误处理");
return;
}
this.isHandlingError = true;
this.lastErrorTime = now;
try {
const { code, message } = this.parseSocketError(error);
console.log("解析的错误信息:", { code, message });
switch (code) {
case "1020": // IP_LIMIT_CONNECT - 本机连接数已达上限
await this.handleConnectionLimitError();
break;
case "1021": // MAX_LIMIT_CONNECT - 服务器连接数已达上限
this.handleServerLimitError(message);
break;
case "1022": // SET_PRINCIPAL_FAIL - 用户身份设置失败
this.handlePrincipalError(message);
break;
case "1023": // GET_PRINCIPAL_FAIL - 用户信息获取失败
this.handlePrincipalError(message);
break;
default:
// 其他错误,检查是否包含连接数上限关键词
if (
message.includes("连接数已达上限") ||
message.includes("本机连接数已达上限")
) {
await this.handleConnectionLimitError();
} else {
// 使用原有的断开重连逻辑
this.handleDisconnect();
}
break;
}
} catch (handleError) {
console.error("处理Socket错误时发生异常:", handleError);
this.handleDisconnect();
} finally {
// 延迟重置处理标志,防止快速重复
setTimeout(() => {
this.isHandlingError = false;
}, 2000);
}
},
/**
* 处理连接数上限错误 (1020)
*/
async handleConnectionLimitError() {
console.log("检测到连接数上限错误,开始强制断开并重连...");
// 清除现有的重连定时器,避免冲突
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.isWebSocketConnected = false;
this.connectionStatus = "error";
// === 移除用户提示:这种错误会自动处理,不需要打扰用户 ===
console.log("💡 检测到连接数上限,后台自动重连中...");
try {
// 1. 强制断开现有连接
this.forceDisconnectAll();
// 2. 等待一段时间让服务器释放连接
await new Promise((resolve) => setTimeout(resolve, 3000));
// 3. 只重连一次,避免无限循环
console.log("尝试重新连接...");
await this.initWebSocket();
if (this.isWebSocketConnected) {
console.log("✅ 客服连接已自动恢复正常");
// === 移除成功提示:自动恢复不需要告知用户 ===
} else {
// 如果重连失败,只在控制台记录,避免满屏提示
console.error("❌ 客服连接重连失败");
}
} catch (error) {
console.error("处理连接数上限错误失败:", error);
// === 减少错误提示:只在控制台记录 ===
}
},
/**
* 处理服务器连接数上限错误 (1021)
*/
handleServerLimitError(message) {
this.isWebSocketConnected = false;
this.connectionStatus = "error";
console.log("服务器连接数已达上限");
// === 减少错误提示:只在控制台记录 ===
console.error("服务器繁忙,连接数已达上限");
// 不进行自动重连,让用户手动刷新
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
},
/**
* 处理用户身份相关错误 (1022, 1023)
*/
handlePrincipalError(message) {
this.isWebSocketConnected = false;
this.connectionStatus = "error";
console.log("用户身份验证失败:", message);
// === 减少错误提示:只在控制台记录 ===
console.error("身份验证失败:", message);
// 不进行自动重连,可能需要重新登录
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
},
/**
* 开始连接验证机制
* 在连接建立后的1分钟内验证连接是否真正可用
* 通过订阅成功和接收任何消息来验证不主动发送ping消息
*/
startConnectionVerification() {
console.log("🔍 启动客服连接验证机制(被动验证)...");
console.log("当前连接状态:", this.connectionStatus);
console.log("当前WebSocket连接状态:", this.isWebSocketConnected);
console.log("当前STOMP连接状态:", this.stompClient?.connected);
this.isConnectionVerified = false;
// 清除之前的验证定时器
this.clearConnectionVerifyTimer();
// 如果已经是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("连接可能不可用");
this.handleConnectionVerificationFailure();
}
}, this.connectionVerifyTimeout); // 60秒超时
console.log("⏲️ 已设置客服1分钟验证超时定时器");
// 注意采用被动验证方式不发送ping消息避免在对话框中显示验证消息
},
/**
* 标记连接已验证
*/
markConnectionVerified() {
if (!this.isConnectionVerified) {
console.log("🎉 客服连接验证成功!清除验证定时器");
this.isConnectionVerified = true;
this.clearConnectionVerifyTimer();
// 确保连接状态是正确的
if (this.connectionStatus !== "connected") {
console.log("📡 修正客服连接状态为connected");
this.connectionStatus = "connected";
}
} 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.clearConnectionVerifyTimer();
// 重置连接状态
this.isWebSocketConnected = false;
this.connectionStatus = "error";
// 2秒后重新连接
setTimeout(() => {
console.log("连接验证失败,开始重新连接...");
this.isHandlingError = false;
// 断开当前连接
if (this.stompClient) {
try {
this.stompClient.disconnect();
} catch (error) {
console.warn("断开连接时出错:", error);
}
}
// 重新连接
this.initWebSocket().catch((error) => {
console.error("重新连接失败:", error);
this.isHandlingError = false;
});
}, 2000);
},
/**
* 检查并确保连接状态
* @returns {Promise<boolean>} 连接是否可用
*/
async checkAndEnsureConnection() {
console.log("🔍 检查客服连接状态...");
// 更新最后活动时间
this.updateLastActivityTime();
// 检查基本连接状态
if (!this.stompClient) {
console.log("❌ STOMP客户端不存在需要重新连接");
return await this.reconnectForSend();
}
// 检查STOMP连接状态
if (!this.stompClient.connected) {
console.log("❌ STOMP连接已断开需要重新连接");
return await this.reconnectForSend();
}
// 检查WebSocket底层连接
if (
this.stompClient.ws &&
this.stompClient.ws.readyState !== WebSocket.OPEN
) {
console.log("❌ WebSocket底层连接异常需要重新连接");
return await this.reconnectForSend();
}
// 检查应用层连接状态
if (!this.isWebSocketConnected || this.connectionStatus !== "connected") {
console.log("❌ 应用层连接状态异常,需要重新连接");
return await this.reconnectForSend();
}
console.log("✅ 客服连接状态良好");
return true;
},
/**
* 为发送消息重新连接
* @returns {Promise<boolean>} 重连是否成功
*/
async reconnectForSend() {
try {
console.log("🔄 开始为发送消息重新连接...");
this.connectionStatus = "connecting";
// 强制断开现有连接
this.forceDisconnectAll();
// 等待一段时间
await new Promise((resolve) => setTimeout(resolve, 1000));
// 重新初始化连接
await this.initWebSocket();
// 检查连接是否成功
if (
this.isWebSocketConnected &&
this.connectionStatus === "connected"
) {
console.log("✅ 重连成功");
return true;
} else {
console.log("❌ 重连失败");
console.error("❌ 重连失败");
return false;
}
} catch (error) {
console.error("重连过程异常:", error);
console.error("❌ 连接异常");
return false;
}
},
/**
* 检查是否是连接错误
* @param {Error} error - 错误对象
* @returns {boolean} 是否是连接错误
*/
isConnectionError(error) {
if (!error) return false;
const errorMessage = error.message || error.toString();
return (
errorMessage.includes("ExecutorSubscribableChannel") ||
errorMessage.includes("NullPointerException") ||
errorMessage.includes("Failed to send message") ||
errorMessage.includes("connection") ||
errorMessage.includes("disconnect") ||
errorMessage.includes("websocket") ||
errorMessage.includes("STOMP")
);
},
/**
* 处理发送消息时的连接错误
* @param {Error} error - 错误对象
*/
async handleConnectionErrorInSend(error) {
console.log("🚨 发送消息时检测到连接错误:", error.message);
// 显示用户友好的错误信息
console.log("🔄 连接已断开,正在重新连接...");
// 重置连接状态
this.isWebSocketConnected = false;
this.connectionStatus = "connecting";
try {
// 尝试重新连接
const reconnected = await this.reconnectForSend();
if (reconnected) {
console.log("✅ 发送消息时自动重连成功");
// === 移除成功提示:自动恢复不需要告知用户 ===
}
} catch (reconnectError) {
console.error("🔄 客服自动重连失败:", reconnectError);
this.connectionStatus = "error";
// === 移除重连失败提示:会继续自动重试,不需要打扰用户 ===
}
},
/**
* 清除连接验证定时器
*/
clearConnectionVerifyTimer() {
if (this.connectionVerifyTimer) {
console.log("🧹 清除客服连接验证定时器");
clearTimeout(this.connectionVerifyTimer);
this.connectionVerifyTimer = null;
} else {
console.log("🔍 没有需要清除的客服验证定时器");
}
},
/**
* 启动心跳检测
*/
startHeartbeat() {
console.log("💓 启动客服心跳检测...");
// 清除之前的心跳定时器
this.stopHeartbeat();
this.heartbeatInterval = setInterval(() => {
if (this.isWebSocketConnected && this.stompClient?.connected) {
this.sendHeartbeat();
} else {
console.warn("💔 客服心跳检测发现连接异常");
this.handleHeartbeatFailure();
}
}, this.heartbeatTimeout);
console.log(
`💓 客服心跳检测已启动,间隔: ${this.heartbeatTimeout / 1000}`
);
},
/**
* 停止心跳检测
*/
stopHeartbeat() {
if (this.heartbeatInterval) {
console.log("💔 停止客服心跳检测");
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
},
/**
* 发送心跳包
*/
sendHeartbeat() {
try {
if (!this.stompClient?.connected) {
console.warn("💔 STOMP未连接无法发送心跳");
this.handleHeartbeatFailure();
return;
}
// 使用STOMP的内置心跳不发送实际消息避免干扰聊天
// 检查WebSocket底层连接状态
if (
this.stompClient.ws &&
this.stompClient.ws.readyState === WebSocket.OPEN
) {
this.lastHeartbeatTime = Date.now();
// === 减少心跳日志:只在异常时输出,正常时静默运行 ===
// 更新活动时间
this.updateLastActivityTime();
} else {
console.warn("💔 WebSocket底层连接异常");
this.handleHeartbeatFailure();
}
} catch (error) {
console.error("💔 客服心跳检测异常:", error);
this.handleHeartbeatFailure();
}
},
/**
* 处理心跳失败
*/
handleHeartbeatFailure() {
console.warn("💔 客服心跳失败,开始重连...");
// 停止心跳
this.stopHeartbeat();
// 标记连接断开
this.isWebSocketConnected = false;
this.connectionStatus = "error";
// 触发重连
this.handleDisconnect();
},
/**
* 启动连接状态检查
*/
startConnectionCheck() {
console.log("🔍 启动客服连接状态检查...");
// 清除之前的检查定时器
this.stopConnectionCheck();
this.connectionCheckInterval = setInterval(() => {
this.performConnectionCheck();
}, this.connectionCheckTimeout);
console.log(
`🔍 客服连接状态检查已启动,间隔: ${
this.connectionCheckTimeout / 1000
}`
);
},
/**
* 停止连接状态检查
*/
stopConnectionCheck() {
if (this.connectionCheckInterval) {
console.log("🔍 停止客服连接状态检查");
clearInterval(this.connectionCheckInterval);
this.connectionCheckInterval = null;
}
},
/**
* 执行连接状态检查
*/
performConnectionCheck() {
// === 减少日志频率:只在检测到问题时输出详细信息 ===
const now = Date.now();
const timeSinceLastHeartbeat = now - this.lastHeartbeatTime;
// 检查基本连接状态
if (
!this.stompClient ||
!this.stompClient.connected ||
!this.isWebSocketConnected
) {
console.warn("🚨 客服连接状态检查:基本连接异常");
this.handleConnectionFailure("基本连接状态异常");
return;
}
// 检查心跳超时3分钟内没有心跳认为连接异常
if (timeSinceLastHeartbeat > 3 * 60 * 1000) {
console.warn("🚨 客服连接状态检查:心跳超时");
this.handleConnectionFailure("心跳超时");
return;
}
// 检查WebSocket底层状态
if (
this.stompClient.ws &&
this.stompClient.ws.readyState !== WebSocket.OPEN
) {
console.warn("🚨 客服连接状态检查WebSocket底层连接异常");
this.handleConnectionFailure("WebSocket底层连接异常");
return;
}
// === 减少正常状态日志:只在异常时输出 ===
},
/**
* 处理连接检查失败
*/
handleConnectionFailure(reason) {
console.warn(`🚨 客服连接失败: ${reason}`);
// 防止重复处理
const now = Date.now();
if (this.isHandlingError && now - this.lastErrorTime < 10000) {
console.log("正在处理连接失败,跳过重复处理");
return;
}
this.isHandlingError = true;
this.lastErrorTime = now;
// 停止所有定时器
this.stopHeartbeat();
this.stopConnectionCheck();
// 标记连接断开
this.isWebSocketConnected = false;
this.connectionStatus = "error";
// === 移除用户提示:后台自动处理,不打扰用户 ===
// 只在控制台记录不显示toast消息
// 触发重连
setTimeout(() => {
this.isHandlingError = false;
this.handleDisconnect();
}, 1000);
},
/**
* 强制断开所有现有连接
*/
forceDisconnectAll() {
try {
console.log("开始强制断开所有连接...");
// 清除重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
// === 新增:清除连接验证定时器 ===
this.clearConnectionVerifyTimer();
// === 新增:停止心跳和连接检查 ===
this.stopHeartbeat();
this.stopConnectionCheck();
if (this.stompClient) {
// 取消所有订阅
if (this.stompClient.subscriptions) {
Object.keys(this.stompClient.subscriptions).forEach((id) => {
try {
this.stompClient.unsubscribe(id);
} catch (error) {
console.log("取消订阅失败:", error);
}
});
}
// 强制断开连接
try {
this.stompClient.deactivate();
} catch (error) {
console.log("断开连接失败:", error);
}
// 等待一小段时间确保连接完全断开
setTimeout(() => {
this.stompClient = null;
}, 100);
}
// 重置连接状态
this.isWebSocketConnected = false;
this.connectionStatus = "disconnected";
console.log("已强制断开所有连接");
} catch (error) {
console.error("强制断开连接失败:", error);
this.stompClient = null;
}
},
// 处理断开连接
handleDisconnect() {
// 如果正在处理特殊错误,不执行普通重连逻辑
if (this.isHandlingError) {
console.log("正在处理特殊错误,跳过普通断开处理");
return;
}
// === 新增:清除连接验证定时器 ===
this.clearConnectionVerifyTimer();
// === 新增:停止心跳和连接检查 ===
this.stopHeartbeat();
this.stopConnectionCheck();
this.isWebSocketConnected = false;
this.connectionStatus = "error";
this.isConnectionVerified = false; // 重置验证状态
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(
`🔄 客服自动重连中 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`
);
this.reconnectTimer = setTimeout(() => {
if (!this.isWebSocketConnected && !this.isHandlingError) {
this.initWebSocket();
}
}, this.reconnectInterval);
} else {
console.log("❌ 达到最大重连次数,停止自动重连");
// === 减少错误提示:只在控制台记录 ===
console.error("❌ 达到最大重连次数,连接失败");
}
},
// 检查并重连
async checkAndReconnect() {
if (!this.isWebSocketConnected) {
console.log("页面恢复可见,尝试重新连接...");
await this.initWebSocket();
}
},
// 开始活动检测
startActivityCheck() {
this.activityCheckInterval = setInterval(() => {
const now = Date.now();
const inactiveTime = now - this.lastActivityTime;
// === 修改:客服系统不应该因为无操作而主动断开连接 ===
// 客服可能需要长时间待机等待用户消息,所以移除自动断开逻辑
// 只在极端情况下超过4小时无任何活动才考虑断开避免僵尸连接
if (inactiveTime > 4 * 60 * 60 * 1000) {
// 4小时
console.log("客服系统4小时无活动断开连接防止僵尸连接");
this.disconnectWebSocket();
}
// 每30分钟记录一次状态便于调试
if (
inactiveTime > 30 * 60 * 1000 &&
inactiveTime % (30 * 60 * 1000) < 60000
) {
console.log(
`客服系统:已无活动 ${Math.floor(
inactiveTime / (60 * 1000)
)} 分钟,连接状态:${this.connectionStatus}`
);
}
}, 60000); // 每分钟检查一次
},
// 更新最后活动时间
updateLastActivityTime() {
this.lastActivityTime = Date.now();
// console.log("客服活动时间已更新"); // 取消注释可用于调试
},
// 获取当前的 UTC 时间
getUTCTime() {
const now = new Date();
return new Date(now.getTime() + now.getTimezoneOffset() * 60000);
},
// 发送消息
async sendMessage() {
// 网络断开时阻止发送消息并提示
if (this.networkStatus !== 'online') {
this.$message({
message: this.$t("chat.networkError") || "网络连接已断开,无法发送消息",
type: "error",
showClose: true
});
return;
}
if (!this.inputMessage.trim() || !this.currentContact || this.sending)
return;
const messageContent = this.inputMessage.trim();
this.inputMessage = "";
this.sending = true;
try {
// === 新增:增强连接状态检查 ===
const connectionCheck = await this.checkAndEnsureConnection();
if (!connectionCheck) {
console.log("客服连接检查失败,无法发送消息");
this.sending = false;
// === 修复:连接失败时恢复输入内容 ===
this.inputMessage = messageContent;
return;
}
// 正确设置接收者类型
const receiveUserType =
this.currentContact.sendUserType !== undefined
? this.currentContact.sendUserType
: 1; // 默认登录用户
const message = {
content: messageContent,
type: 1, // 1 表示文字消息
email: this.currentContact.name,
receiveUserType: receiveUserType,
roomId: this.currentContactId,
};
// 发送消息到服务器
this.stompClient.send(
"/point/send/message/to/user",
{},
JSON.stringify(message)
);
// === 修复:立即添加消息到本地聊天记录,避免快速发送时消息不显示 ===
const currentTime = new Date().toISOString();
const localMessageId = `local_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
console.log("📤 发送消息 - 立即添加到本地:", {
currentContactId: this.currentContactId,
currentContactName: this.currentContact?.name,
messageContent: messageContent,
currentTime: currentTime,
localMessageId: localMessageId,
});
// 立即添加消息到本地聊天记录
this.addMessageToChat(
{
id: localMessageId, // 使用临时ID服务器回传时会更新
sender: this.$t("chat.my") || "我",
avatar: "iconfont icon-icon28",
content: messageContent,
time: currentTime,
isSelf: true,
isImage: false,
type: 1,
roomId: this.currentContactId,
isLocalMessage: true, // 标记为本地消息
},
true
); // 标记为用户主动发送的消息
// 发送消息后立即更新联系人时间
this.updateContactLastMessage({
roomId: this.currentContactId,
content: messageContent,
isImage: false,
time: currentTime,
});
// 重置当前聊天室的未读消息数
const contact = this.contacts.find(
(c) => c.roomId === this.currentContactId
);
if (contact) {
contact.unread = 0;
}
this.sending = false;
this.$nextTick(() => {
this.scrollToBottom();
});
} catch (error) {
console.error("客服发送消息失败:", error);
this.sending = false;
// === 新增:处理特定错误类型 ===
if (this.isConnectionError(error)) {
console.log("检测到客服连接错误,开始重连...");
this.handleConnectionErrorInSend(error);
} else {
// 检查是否是Socket特定错误
const { code } = this.parseSocketError(error);
if (["1020", "1021", "1022", "1023"].includes(code)) {
// 如果是Socket特定错误不重复处理因为连接错误处理器会处理
return;
}
// === 优化错误提示:只显示用户需要知道的错误 ===
console.error("💬 客服发送消息错误详情:", error);
// 仅在明确需要用户操作时才显示提示
if (
error.message &&
(error.message.includes("connection") ||
error.message.includes("WebSocket") ||
error.message.includes("STOMP"))
) {
console.log("🔄 发送失败,触发自动重连机制");
// 连接错误会被自动处理,不需要用户提示
} else {
// 其他错误可能需要用户重试,但减少提示频率
console.error("💬 发送消息失败,需要用户重试");
// 只在非连接错误时才提示用户
this.$message.error(this.$t("chat.failInSend") || "发送失败,请重试");
}
}
}
},
//换行消息显示处理
formatMessageContent(content) {
if (!content) return "";
// 防止XSS建议先做转义如有需要
return content.replace(/\n/g, "<br>");
},
// 订阅个人消息队列
subscribeToPersonalMessages() {
if (!this.stompClient || !this.wsConnected) return;
this.stompClient.subscribe(
`/user/queue/${this.userEmail}`,
this.handleIncomingMessage
);
},
// 处理接收到的消息
async handleIncomingMessage(message) {
try {
// === 收到消息说明连接正常工作,标记为已验证并更新活动时间 ===
console.log("🎉 客服收到消息,标记连接已验证");
this.markConnectionVerified();
this.updateLastActivityTime(); // 收到消息也是一种活动
this.lastHeartbeatTime = Date.now(); // 更新心跳时间
const msg = JSON.parse(message.body);
console.log("客服收到的消息", msg);
// === 修复:处理后端返回的时间格式 ===
const serverTime = msg.createTime || msg.sendTime;
let formattedTime;
if (serverTime) {
if (typeof serverTime === "string" && serverTime.includes("T")) {
// 后端返回的标准格式 "2025-06-12T02:22:39",直接使用
formattedTime = serverTime;
} else if (
typeof serverTime === "number" ||
/^\d+$/.test(serverTime)
) {
// 如果是时间戳转换为ISO字符串
formattedTime = new Date(parseInt(serverTime)).toISOString();
} else {
// 其他情况尝试解析
formattedTime = new Date(serverTime).toISOString();
}
} else {
// 如果没有服务器时间使用本地时间的ISO格式
formattedTime = new Date().toISOString();
}
const messageData = {
id: msg.id,
sender:
msg.sendUserType === this.userType &&
msg.sendEmail === this.userEmail
? this.$t("chat.my") || "我"
: msg.sendEmail || this.$t("chat.unknownSender") || "未知发送者",
avatar:
msg.sendUserType === 2
? "iconfont icon-icon28"
: "iconfont icon-user",
content: msg.content,
time: formattedTime,
isSelf:
msg.sendUserType === this.userType &&
msg.sendEmail === this.userEmail,
isImage: msg.type === 2,
type: msg.type,
roomId: msg.roomId,
sendUserType: msg.sendUserType,
isCreate: msg.isCreate,
clientReadNum: msg.clientReadNum,
};
// === 处理回环消息:如果是自己发送的消息,检查是否为本地消息的服务器确认 ===
if (messageData.isSelf) {
const roomMessages = this.messages[messageData.roomId] || [];
// 查找是否有对应的本地消息(相同内容且时间接近)
const localMessageIndex = roomMessages.findIndex((msg) => {
if (!msg.isLocalMessage || msg.content !== messageData.content)
return false;
const localTime = new Date(msg.time).getTime();
const serverTime = new Date(messageData.time).getTime();
const timeDiff = Math.abs(serverTime - localTime);
// 如果时间差在30秒内认为是同一条消息
return timeDiff < 30000;
});
if (localMessageIndex !== -1) {
// 找到对应的本地消息,更新为服务器消息
// 更新消息ID和移除本地标记
this.$set(roomMessages, localMessageIndex, {
...roomMessages[localMessageIndex],
id: messageData.id,
time: messageData.time, // 使用服务器时间
isLocalMessage: false, // 移除本地标记
});
return; // 不需要添加新消息
}
// 如果没找到对应的本地消息,检查是否重复
const isDuplicate = this.checkDuplicateMessage(messageData);
if (isDuplicate) {
return;
}
}
// 更新或创建聊天室
const existingContact = this.contacts.find(
(c) => c.roomId === messageData.roomId
);
if (!existingContact) {
// 如果聊天室不存在,创建新的聊天室
const newContact = {
roomId: messageData.roomId,
name: messageData.sender,
lastMessage: messageData.isImage
? this.$t(`chat.picture2`) || "[图片]"
: messageData.content,
lastTime: messageData.time, // 直接使用 createTime
unread: messageData.isSelf ? 0 : 1, // 如果是自己发送的,不增加未读数
important: false,
isGuest: msg.sendUserType === 0,
sendUserType: messageData.sendUserType,
isManualCreated: true,
};
// 不使用unshift而是添加到数组中让排序决定位置
this.contacts.push(newContact);
this.$set(this.messages, messageData.roomId, []);
} else {
// 如果聊天室已存在,更新最后一条消息和时间
existingContact.lastMessage = messageData.isImage
? this.$t(`chat.picture2`) || "[图片]"
: messageData.content;
existingContact.lastTime = messageData.time; // 直接使用 createTime
}
// 添加消息到聊天记录
if (!this.messages[messageData.roomId]) {
this.$set(this.messages, messageData.roomId, []);
}
this.messages[messageData.roomId].push({
id: messageData.id,
sender: messageData.sender,
avatar: messageData.avatar,
content: messageData.content,
time: messageData.time,
isSelf: messageData.isSelf,
isImage: messageData.isImage,
type: messageData.type,
roomId: messageData.roomId,
});
// 智能排序:只在检测到顺序混乱时才排序
if (this.needsResort(this.messages[messageData.roomId])) {
this.messages[messageData.roomId] = this.sortMessages(
this.messages[messageData.roomId]
);
}
// === 优化未读数逻辑 ===
if (messageData.roomId === this.currentContactId) {
if (this.userViewHistory) {
// 正在查看历史不清零未读数显示clientReadNum
const contact = this.contacts.find(
(c) => c.roomId === messageData.roomId
);
if (contact) {
contact.unread =
messageData.clientReadNum || contact.unread + 1 || 1;
this.setUnreadCount(messageData.roomId, contact.unread);
}
} else {
// 在底部,自动已读
await this.markMessagesAsRead(messageData.roomId);
}
} else {
// 非当前会话,未读数递增
if (!messageData.isSelf) {
const contact = this.contacts.find(
(c) => c.roomId === messageData.roomId
);
if (contact) {
contact.unread =
messageData.clientReadNum || contact.unread + 1 || 1;
this.setUnreadCount(messageData.roomId, contact.unread);
}
}
}
// 重新排序联系人列表
this.sortContacts();
} catch (error) {
console.error("处理新消息失败:", error);
}
},
// 处理联系人列表滚动
handleContactListScroll(e) {
const container = e.target;
// 判断是否滚动到底部允许2px的误差
if (
container.scrollHeight - container.scrollTop - container.clientHeight <
2
) {
this.loadMoreContacts();
}
},
// 加载更多联系人
async loadMoreContacts() {
if (this.isLoadingMoreContacts) return;
const lastContact = this.contacts[this.contacts.length - 1];
if (!lastContact) return;
this.isLoadingMoreContacts = true;
try {
const formatDateTime = (date) => {
if (!date) return null;
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
const seconds = String(d.getSeconds()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const requestData = {
sendDateTime: formatDateTime(lastContact.lastTime),
userType: 2,
email: this.userEmail,
};
const response = await getRoomList(requestData);
if (response?.code === 200) {
const newContacts = response.rows.map((room) => {
const existingContact = this.contacts.find(
(c) => c.roomId === room.id
);
const manualRoom = this.manualCreatedRooms.find(
(c) => c.roomId === room.id
);
const isImportant =
room.flag === 1
? true
: room.flag === 0
? false
: room.important === 1
? true
: existingContact
? existingContact.important
: false;
// === 修复:处理服务器返回的时间格式 ===
const serverTime = room.lastUserSendTime;
let lastTime;
if (serverTime) {
if (typeof serverTime === "string" && serverTime.includes("T")) {
// 后端返回的标准格式,直接使用
lastTime = serverTime;
} else if (
typeof serverTime === "number" ||
/^\d+$/.test(serverTime)
) {
// 时间戳格式转换为ISO字符串
lastTime = new Date(parseInt(serverTime)).toISOString();
} else {
// 其他格式,尝试解析
lastTime = new Date(serverTime).toISOString();
}
} else {
// 如果没有服务器时间,使用本地时间
lastTime = new Date().toISOString();
}
return {
roomId: room.id,
name: room.userEmail || this.$t(`chat.Unnamed`) || "未命名聊天室",
avatar: this.getDefaultAvatar(
room.roomName || this.$t(`chat.Unnamed`) || "未命名聊天室"
),
lastMessage:
room.lastMessage ||
(existingContact
? existingContact.lastMessage
: this.$t(`chat.noNewsAtTheMoment`) || "暂无消息"),
lastTime: lastTime,
unread: existingContact?.unread ?? room.clientReadNum ?? 0,
important: isImportant,
isManualCreated: manualRoom ? true : false,
sendUserType: room.sendUserType,
isGuest: room.sendUserType === 0,
};
});
const uniqueNewContacts = newContacts.filter(
(newContact) =>
!this.contacts.some(
(existingContact) =>
existingContact.roomId === newContact.roomId
)
);
if (uniqueNewContacts.length > 0) {
this.contacts = [...this.contacts, ...uniqueNewContacts];
this.sortContacts();
}
} else {
this.$message({
message: this.$t("chat.contactFailed") || "加载更多联系人失败",
type: "error",
duration: 3000,
showClose: true,
});
}
} catch (error) {
console.error("5858", error);
} finally {
this.isLoadingMoreContacts = false;
}
},
// 处理新聊天室创建
handleNewChatRoom(messageData) {
// 检查是否已存在该聊天室
const existingContact = this.contacts.find(
(c) => c.roomId === messageData.roomId
);
if (!existingContact) {
// 创建新的聊天室对象
const newContact = {
roomId: messageData.roomId,
name: messageData.sender,
// 修改这里:使用实际收到的消息内容作为最后一条消息
lastMessage: messageData.isImage
? this.$t(`chat.picture2`) || "[图片]"
: messageData.content,
lastTime: messageData.time,
unread: 1,
important: false,
isGuest: messageData.sendUserType === 0,
sendUserType: messageData.sendUserType,
isManualCreated: true,
clientReadNum: messageData.clientReadNum,
};
// 添加到聊天列表
this.contacts.push(newContact);
// 初始化该聊天室的消息数组
this.$set(this.messages, messageData.roomId, []);
// 将当前消息添加到聊天记录中
this.messages[messageData.roomId].push({
id: messageData.id,
sender: messageData.sender,
avatar:
messageData.sendUserType === 2
? "iconfont icon-icon28"
: "iconfont icon-user",
content: messageData.content,
time: messageData.time,
isSelf: false,
isImage: messageData.type === 2,
type: messageData.type,
roomId: messageData.roomId,
});
// 新聊天室的首条消息通常不需要排序,但保险起见检查一下
if (this.needsResort(this.messages[messageData.roomId])) {
this.messages[messageData.roomId] = this.sortMessages(
this.messages[messageData.roomId]
);
}
// 保存到手动创建的聊天室列表
this.manualCreatedRooms.push(newContact);
this.saveManualCreatedRooms();
// 重新排序联系人列表
this.sortContacts();
}
},
saveManualCreatedRooms() {
localStorage.setItem(
"manualCreatedRooms",
JSON.stringify(this.manualCreatedRooms)
);
},
// 从 localStorage 加载手动创建的聊天室
async loadManualCreatedRooms() {
try {
const savedRooms = localStorage.getItem("manualCreatedRooms");
if (savedRooms) {
this.manualCreatedRooms = JSON.parse(savedRooms);
// 将手动创建的聊天室添加到当前联系人列表
for (const room of this.manualCreatedRooms) {
const exists = this.contacts.find((c) => c.roomId === room.roomId);
if (!exists) {
this.contacts.push({
...room,
lastTime: room.lastTime || new Date().toISOString(),
});
// 初始化消息数组
if (!this.messages[room.roomId]) {
this.$set(this.messages, room.roomId, []);
// 如果需要,可以在这里加载该聊天室的历史消息
await this.loadMessages(room.roomId);
}
}
}
this.sortContacts();
}
} catch (error) {
console.error("加载手动创建的聊天室失败:", error);
}
},
// 添加新方法:创建新聊天室
async createNewChatRoom(messageData) {
try {
// 调用后端 API 创建新的聊天室
const response = await createChatRoom({
userEmail: messageData.sender,
userType: messageData.sendUserType,
});
if (response && response.code === 200) {
const newRoom = {
userEmail: messageData.sender,
roomId: response.data.roomId,
lastMessage: messageData.content,
lastMessageTime: messageData.time,
unreadCount: messageData.clientReadNum || 0,
userType: messageData.sendUserType,
};
this.chatRooms.unshift(newRoom);
return newRoom;
}
} catch (error) {
console.error("创建新聊天室失败:", error);
throw error;
}
},
// 更新聊天室列表
updateChatRoomList(messageData) {
const roomIndex = this.chatRooms.findIndex(
(room) => room.roomId === messageData.roomId
);
if (roomIndex !== -1) {
// 更新现有聊天室信息
this.chatRooms[roomIndex] = {
...this.chatRooms[roomIndex],
lastMessage: messageData.content,
lastMessageTime: messageData.time,
unreadCount:
messageData.clientReadNum || this.chatRooms[roomIndex].unreadCount,
};
// 将更新的聊天室移到列表顶部
const updatedRoom = this.chatRooms.splice(roomIndex, 1)[0];
this.chatRooms.unshift(updatedRoom);
}
},
// 修改标记为已读方法,添加参数支持
async markMessagesAsRead(roomId = this.currentContactId) {
if (!roomId) return;
try {
const data = {
roomId: roomId,
userType: 2,
};
const response = await getReadMessage(data);
if (response && response.code === 200) {
console.log("消息已标记为已读");
// 更新联系人列表中的未读计数
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact) {
contact.unread = 0;
this.setUnreadCount(roomId, 0);
}
} else {
console.warn("标记消息已读失败", response);
}
} catch (error) {
console.error("标记消息已读出错:", error);
}
},
// 解析 UTC 时间字符串
parseUTCTime(timeStr) {
if (!timeStr) return new Date(); // 如果没有时间,返回当前时间
try {
return new Date(timeStr); // 直接解析 UTC 时间
} catch (error) {
console.error("解析时间错误:", error);
return new Date();
}
},
// 获取聊天室列表
async fetchRoomList() {
try {
this.loadingRooms = true;
const requestData = {
lastTime: null,
userType: 2,
email: this.userEmail,
};
const response = await getRoomList(requestData);
if (response?.code === 200) {
const newContacts = response.rows.map((room) => {
const existingContact = this.contacts.find(
(c) => c.roomId === room.id
);
const manualRoom = this.manualCreatedRooms.find(
(c) => c.roomId === room.id
);
const isImportant =
room.flag === 1
? true
: room.flag === 0
? false
: room.important === 1
? true
: existingContact
? existingContact.important
: false;
// === 修复:处理服务器返回的时间格式 ===
const serverTime = room.lastUserSendTime || room.createTime;
let lastTime;
if (serverTime) {
if (typeof serverTime === "string" && serverTime.includes("T")) {
// 后端返回的标准格式,直接使用
lastTime = serverTime;
} else if (
typeof serverTime === "number" ||
/^\d+$/.test(serverTime)
) {
// 时间戳格式转换为ISO字符串
lastTime = new Date(parseInt(serverTime)).toISOString();
} else {
// 其他格式,尝试解析
lastTime = new Date(serverTime).toISOString();
}
} else {
// 如果没有服务器时间,使用本地时间
lastTime = new Date().toISOString();
}
return {
roomId: room.id,
name: room.userEmail || this.$t(`chat.Unnamed`) || "未命名聊天室",
avatar: this.getDefaultAvatar(
room.roomName || this.$t(`chat.Unnamed`) || "未命名聊天室"
),
lastMessage:
room.lastMessage ||
(existingContact
? existingContact.lastMessage
: this.$t(`chat.noNewsAtTheMoment`) || "暂无消息"),
lastTime: lastTime, // 已经确保不为空
unread: existingContact?.unread ?? room.clientReadNum ?? 0,
important: isImportant,
isManualCreated: manualRoom ? true : false,
sendUserType: room.sendUserType,
isGuest: room.sendUserType === 0,
};
});
this.contacts = newContacts;
this.sortContacts();
}
} catch (error) {
// 判断是否为主动取消
if (
error &&
(error.message === "canceled" ||
error.message === "Cancel" ||
error.message?.includes("canceled"))
) {
// 主动取消的请求,不提示
return;
}
console.error("获取聊天室列表异常:", error);
this.$message({
message: this.$t("chat.listException") || "获取聊天室列表异常",
type: "error",
duration: 3000,
showClose: true,
});
} finally {
this.loadingRooms = false;
}
},
// 加载更多历史消息 - 简化版本
async loadMoreHistory() {
if (!this.currentContactId) return;
// 获取当前已加载的消息列表
const currentMsgs = this.messages[this.currentContactId] || [];
// 检查是否有历史消息记录
if (currentMsgs.length === 0) {
this.hasMoreHistory = false;
this.noMoreHistoryMessage =
this.$t("chat.noMoreHistory") || "没有更多历史消息";
return;
}
// 获取ID最小的消息最早的消息作为查询参数
const earliestMessage = this.getEarliestMessage(currentMsgs);
if (!earliestMessage || !earliestMessage.id) {
this.hasMoreHistory = false;
this.noMoreHistoryMessage =
this.$t("chat.noMoreHistory") || "没有更多历史消息";
return;
}
// 使用最早消息的id作为查询参数
this.history7Params.id = earliestMessage.id;
this.history7Params.roomId = this.currentContactId;
this.history7Params.email = this.userEmail;
try {
this.messagesLoading = true;
const response = await getHistory7(this.history7Params);
// 简化:如果接口返回数据为空,就显示没有更多历史消息
if (
!response ||
response.code !== 200 ||
!response.data ||
response.data.length === 0
) {
this.hasMoreHistory = false;
this.noMoreHistoryMessage =
this.$t("chat.noMoreHistory") || "没有更多历史消息";
return;
}
// 过滤当前聊天室的消息
const filteredMessages = response.data.filter((msg) => {
return (
msg.roomId == this.currentContactId ||
String(msg.roomId) === String(this.currentContactId)
);
});
// 如果过滤后没有消息,显示没有更多历史消息
if (filteredMessages.length === 0) {
this.hasMoreHistory = false;
this.noMoreHistoryMessage =
this.$t("chat.noMoreHistory") || "没有更多历史消息";
return;
}
let moreMessages = filteredMessages.map((msg) => ({
id: msg.id,
sender:
msg.isSelf === 1
? this.$t("chat.my") || "我"
: msg.sendEmail || this.$t("chat.unknownSender") || "未知发送者",
avatar: "iconfont icon-icon28",
content: msg.content,
time: msg.createTime,
isSelf: msg.isSelf === 1,
isImage: msg.type === 2,
isRead: msg.isRead === 1,
type: msg.type,
roomId: msg.roomId,
}));
// 过滤掉重复的消息
const currentMessageIds = (
this.messages[this.currentContactId] || []
).map((m) => m.id);
const uniqueMessages = moreMessages.filter(
(msg) => !currentMessageIds.includes(msg.id)
);
// 如果去重后没有新消息,显示没有更多历史消息
if (uniqueMessages.length === 0) {
this.hasMoreHistory = false;
this.noMoreHistoryMessage =
this.$t("chat.noMoreHistory") || "没有更多历史消息";
return;
}
// 使用去重后的消息
moreMessages = this.sortMessages(uniqueMessages);
// 追加到当前消息列表前面
const oldMessages = this.messages[this.currentContactId] || [];
this.$set(this.messages, this.currentContactId, [
...moreMessages,
...oldMessages,
]);
} catch (error) {
console.error("加载更多历史消息失败:", error);
this.$message.error(
this.$t("chat.historicalFailure") || "加载更多历史消息失败"
);
} finally {
this.messagesLoading = false;
}
},
// 选择联系人
async selectContact(roomId) {
if (this.currentContactId === roomId) return;
// === 新增:更新活动时间 ===
this.updateLastActivityTime();
try {
this.messagesLoading = true; // 显示加载状态
this.currentContactId = roomId;
this.userViewHistory = false;
// 简化:切换聊天室时重置历史消息状态,显示加载按钮
this.hasMoreHistory = true;
this.noMoreHistoryMessage = "";
// 重置分页参数
this.history7Params = {
id: "", // 首次加载为空
// pageNum: 1, // 首次页码为1
roomId: roomId,
userType: 2,
};
// 加载历史消息
await this.loadMessages(roomId);
// 标记消息为已读
await this.markMessagesAsRead(roomId);
} catch (error) {
console.error("选择联系人失败:", error);
this.$message({
message: this.$t("chat.loadFailed") || "加载失败",
type: "error",
duration: 3000,
showClose: true,
});
} finally {
this.messagesLoading = false;
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
}
},
//判断是否在聊天框底部
isAtBottom() {
const container = this.$refs.messageContainer;
if (!container) return true;
// 允许2px误差
return (
container.scrollHeight - container.scrollTop - container.clientHeight <
2
);
},
// 加载聊天消息
async loadMessages(roomId) {
if (!roomId) return;
try {
console.log(this.userEmail, "加载聊天消息");
this.history7Params.email = this.userEmail;
this.history7Params.roomId = roomId;
const response = await getHistory7(this.history7Params);
if (response?.code === 200 && response.data) {
// 处理消息数据
let roomMessages = response.data
.filter((msg) => msg.roomId == roomId)
.map((msg) => ({
id: msg.id,
sender:
msg.isSelf === 1
? this.$t("chat.my") || "我"
: msg.sendEmail ||
this.$t("chat.unknownSender") ||
"未知发送者",
avatar:
msg.sendUserType == 2
? "iconfont icon-icon28"
: "iconfont icon-user",
content: msg.content,
time: msg.createTime,
isSelf: msg.isSelf === 1,
isImage: msg.type === 2,
isRead: msg.isRead === 1,
type: msg.type,
roomId: msg.roomId,
sendUserType: msg.sendUserType,
}));
// 使用智能排序ID为主时间为辅
roomMessages = this.sortMessages(roomMessages);
// 更新消息列表
this.$set(this.messages, roomId, roomMessages);
// 更新联系人的最后消息时间(使用最新消息的时间)
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact && roomMessages.length > 0) {
const latestMessageTime =
roomMessages[roomMessages.length - 1].time;
contact.lastTime = latestMessageTime;
}
// 更新联系人的未读状态
if (contact) {
contact.unread = 0;
}
// 简化:初始加载时不需要特殊处理,保持默认状态
} else {
// 如果没有消息数据,初始化为空数组
this.$set(this.messages, roomId, []);
if (response?.code !== 200) {
this.$message({
message: this.$t("chat.recordFailed") || "加载聊天记录失败",
type: "error",
duration: 3000,
showClose: true,
});
}
}
} catch (error) {
console.error("加载消息异常:", error);
// this.$message({
// message: this.$t("chat.messageException") || "加载消息异常",
// type: "error",
// duration: 3000,
// showClose: true,
// });
this.$set(this.messages, roomId, []);
}
},
/**
* 检查是否为重复消息(用于处理回环消息)
* @param {Object} messageData - 消息数据
* @returns {boolean} 是否为重复消息
*/
checkDuplicateMessage(messageData) {
const roomMessages = this.messages[messageData.roomId];
if (!roomMessages) return false;
// 检查是否已存在相同ID的消息
if (
messageData.id &&
roomMessages.some((msg) => msg.id === messageData.id)
) {
console.log("🔍 发现相同ID的消息判定为重复:", messageData.id);
return true;
}
// 检查最近30秒内是否有相同内容的消息缩短时间窗口提高准确性
const thirtySecondsAgo = Date.now() - 30 * 1000;
const serverMsgTime = new Date(messageData.time).getTime();
return roomMessages.some((msg) => {
// 跳过本地消息的检查,因为本地消息会被服务器消息替换
if (msg.isLocalMessage) return false;
if (!msg.isSelf || msg.content !== messageData.content) 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: messageData.time,
timeDiff: timeDiff,
content: messageData.content.substring(0, 50),
});
return true;
}
return false;
});
},
/**
* 添加消息到聊天记录
* @param {Object} messageData - 消息数据
* @param {boolean} isSentByUser - 是否为用户主动发送的消息,影响滚动行为
*/
addMessageToChat(messageData, isSentByUser = false) {
const roomId = messageData.roomId || this.currentContactId;
if (!this.messages[roomId]) {
this.$set(this.messages, roomId, []);
}
// === ChatWidget式push不做本地排序 ===
const message = {
id: messageData.id || Date.now(),
sender: messageData.sender,
avatar:
messageData.avatar ||
(messageData.isSelf ? "iconfont icon-icon28" : "iconfont icon-user"),
content: messageData.content,
time: messageData.time || new Date(),
isSelf: messageData.isSelf,
isImage: messageData.isImage || false,
type: messageData.type || 1,
roomId: roomId,
isRead: messageData.isRead || false,
isLocalMessage: messageData.isLocalMessage || false,
};
this.messages[roomId].push(message);
// 更新最后一条消息
this.updateContactLastMessage({
roomId: roomId,
content: message.isImage
? this.$t("chat.picture2") || "[图片]"
: message.content,
isImage: message.isImage,
time: message.time,
});
// 滚动逻辑同原实现
if (roomId === this.currentContactId) {
if (isSentByUser) {
this.$nextTick(() => {
this.scrollToBottom(true);
this.userViewHistory = false;
});
} else if (!this.userViewHistory) {
this.$nextTick(() => {
this.scrollToBottom();
});
}
}
},
// 处理图片上传
async handleImageUpload(event) {
// 检查是否有选中的联系人和 WebSocket 连接
if (!this.currentContact) {
this.$message({
message: this.$t("chat.chooseFirst") || "请先选择联系人",
type: "error",
duration: 3000,
showClose: true,
});
return;
}
if (!this.stompClient || !this.isWebSocketConnected) {
this.$message({
message:
this.$t("chat.chatDisconnected") ||
"聊天连接已断开,请刷新页面重试",
type: "error",
duration: 3000,
showClose: true,
});
return;
}
const file = event.target.files[0];
if (!file) return;
// 检查是否为图片
if (!file.type.startsWith("image/")) {
this.$message({
message: this.$t("chat.onlyImages") || "只能上传图片文件!",
type: "error",
duration: 3000,
showClose: true,
});
return;
}
// 检查文件大小 (限制为5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
this.$message({
message: this.$t("chat.imageTooLarge") || "图片大小不能超过5MB!",
type: "error",
duration: 3000,
showClose: true,
});
return;
}
this.sending = true;
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);
console.log("✅ 图片发送成功");
// === 移除发送成功提示:图片已在聊天中显示,不需要额外提示 ===
} else {
throw new Error(response.data.msg || "上传失败");
}
} catch (error) {
console.error("上传图片异常:", error);
this.$message({
message: this.$t("chat.pictureFailed") || "图片发送失败,请重试",
type: "error",
duration: 3000,
showClose: true,
});
} finally {
this.sending = false;
// 清空文件选择器
this.$refs.imageInput.value = "";
}
},
// 发送图片消息
async sendImageMessage(imageUrl) {
try {
// === 新增:检查连接状态 ===
const connectionCheck = await this.checkAndEnsureConnection();
if (!connectionCheck) {
console.log("客服图片发送连接检查失败");
// === 减少错误提示:连接错误会自动重连 ===
console.error("❌ 连接异常,图片发送失败");
return;
}
const message = {
type: 2, // 2 表示图片消息
email: this.currentContact.name,
receiveUserType: this.currentContact.sendUserType || 1,
roomId: this.currentContactId,
content: imageUrl, // 使用接口返回的url
};
this.stompClient.send(
"/point/send/message/to/user",
{},
JSON.stringify(message)
);
// === 修复:立即添加图片消息到本地聊天记录 ===
const currentTime = new Date().toISOString();
const localImageId = `local_img_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
console.log("📤 发送图片 - 立即添加到本地:", {
currentContactId: this.currentContactId,
imageUrl: imageUrl,
currentTime: currentTime,
localImageId: localImageId,
});
// 立即添加图片消息到本地聊天记录
this.addMessageToChat(
{
id: localImageId, // 使用临时ID
sender: this.$t("chat.my") || "我",
avatar: "iconfont icon-icon28",
content: imageUrl,
time: currentTime,
isSelf: true,
isImage: true,
type: 2,
roomId: this.currentContactId,
isLocalMessage: true, // 标记为本地消息
},
true
); // 标记为用户主动发送的消息
this.updateContactLastMessage({
roomId: this.currentContactId,
content: imageUrl,
isImage: true,
time: currentTime,
});
} catch (error) {
console.error("发送图片消息失败:", error);
// === 新增:处理连接错误 ===
if (this.isConnectionError(error)) {
console.log("图片发送时检测到连接错误,开始重连...");
this.handleConnectionErrorInSend(error);
} else {
// === 减少错误提示:只在非连接错误时提示 ===
console.error("💬 图片发送失败,需要用户重试");
this.$message.error(this.$t("chat.pictureFailed") || "发送图片消息失败,请重试");
}
}
},
// 更新联系人最后一条消息
updateContactLastMessage(message) {
// 增强查找逻辑:同时支持精确匹配和部分匹配
let contact = this.contacts.find((c) => c.roomId === message.roomId);
// 如果找不到,尝试通过名称查找(兼容旧数据)
if (!contact && typeof message.roomId === "string") {
contact = this.contacts.find(
(c) =>
(c.name && c.name.includes(message.roomId)) ||
(c.roomId && c.roomId.includes(message.roomId))
);
}
if (contact) {
const oldTime = contact.lastTime;
const newTime = message.time || new Date().toISOString();
// 更新联系人信息
contact.lastMessage = message.isImage
? this.$t("chat.picture2") || "[图片]"
: message.content;
contact.lastTime = newTime;
console.log("⚡ updateContactLastMessage - 执行时间更新:", {
contactName: contact.name,
roomId: message.roomId,
oldTime: oldTime,
newTime: newTime,
messageTime: message.time,
isImage: message.isImage,
contactsTotal: this.contacts.length,
});
// 强制触发Vue响应式更新
this.$set(contact, "lastTime", newTime);
this.$set(contact, "lastMessage", contact.lastMessage);
// 重新排序联系人列表
this.sortContacts();
// 强制触发DOM更新
this.$nextTick(() => {
this.$forceUpdate();
});
} else {
console.error("❌ updateContactLastMessage - 未找到联系人:", {
searchRoomId: message.roomId,
messageContent: message.content,
allContacts: this.contacts.map((c) => ({
roomId: c.roomId,
name: c.name,
lastTime: c.lastTime,
})),
});
}
},
// 增加未读消息计数
incrementUnreadCount(roomId, readNum = 1) {
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact) {
// 如果有指定未读数直接设置否则增加1
if (readNum > 1) {
contact.unread = readNum;
} else {
contact.unread = (contact.unread || 0) + 1;
}
}
},
// 预览图片
previewImage(imageUrl) {
this.previewImageUrl = imageUrl;
this.previewVisible = true;
},
// 标记聊天为重要/非重要
async toggleImportant(roomId, important) {
if (!roomId) return;
try {
// 发送请求,使用 id 和 flag 参数
const response = await getUpdateRoom({
id: roomId,
flag: important ? 1 : 0, // 1代表重要0代表不重要
});
if (response && response.code === 200) {
// 更新本地数据
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact) {
contact.important = important;
}
// 重新排序联系人列表,使重要的排在前面
this.sortContacts();
console.log(important ? "✅ 已标记为重要聊天" : "✅ 已取消重要标记");
// === 移除标记提示:状态变化在页面上已可见 ===
} else {
this.$message({
message:
response?.msg || this.$t("chat.markingFailed") || "标记操作失败",
type: "error",
duration: 3000,
showClose: true,
});
}
} catch (error) {
console.error("标记聊天状态异常:", error);
// this.$message({
// message: this.$t("chat.markingFailed") || "标记操作失败,请重试",
// type: "error",
// duration: 3000,
// showClose: true,
// });
}
},
/**
* 解析和标准化时间确保正确处理UTC时间
* @param {string|Date|null} timeValue - 时间值
* @returns {number} - 时间戳,如果无效则返回当前时间
*/
parseTimeForSort(timeValue) {
if (!timeValue) {
// 如果没有时间,使用当前时间(确保新聊天室排在前面)
return Date.now();
}
let timestamp;
if (typeof timeValue === "string") {
// 处理UTC时间字符串
let timeStr = timeValue;
// 如果时间字符串不包含时区信息假设它是UTC时间
if (
!timeStr.includes("Z") &&
!timeStr.includes("+") &&
!timeStr.includes("-")
) {
timeStr += "Z";
}
timestamp = new Date(timeStr).getTime();
} else if (timeValue instanceof Date) {
timestamp = timeValue.getTime();
} else {
// 其他情况使用当前时间
timestamp = Date.now();
}
// 如果解析失败NaN使用当前时间
if (isNaN(timestamp)) {
timestamp = Date.now();
}
return timestamp;
},
// 修复所有联系人的时间问题
fixContactTimes() {
this.contacts.forEach((contact) => {
if (!contact.lastTime) {
const currentTime = new Date().toISOString();
console.warn("🔧 修复联系人空时间:", {
contactName: contact.name,
roomId: contact.roomId,
fixedTime: currentTime,
});
this.$set(contact, "lastTime", currentTime);
}
});
},
// 根据重要性对联系人列表排序
sortContacts() {
// 先修复空时间问题
this.fixContactTimes();
// 使用新的智能排序
this.contacts = this.sortContactsByTime(this.contacts);
},
// 滚动到底部
scrollToBottom(force = false) {
const container = this.$refs.messageContainer;
if (!container) return;
// 使用 nextTick 确保 DOM 更新后再滚动
this.$nextTick(() => {
// 添加一个小延时确保内容完全渲染
setTimeout(() => {
const scrollOptions = {
top: container.scrollHeight,
behavior: force ? "auto" : "smooth",
};
try {
container.scrollTo(scrollOptions);
} catch (error) {
// 如果平滑滚动不支持,则直接设置
container.scrollTop = container.scrollHeight;
}
// 滚动完成后隐藏按钮
if (force) {
this.showScrollButton = false;
}
}, 100);
});
},
// 判断是否显示时间
showMessageTime(index) {
// 显示时间的逻辑第一条消息或距离上一条消息超过5分钟
if (index === 0) return true;
const currentMsg = this.currentMessages[index];
const prevMsg = this.currentMessages[index - 1];
if (!currentMsg.time || !prevMsg.time) return false;
const currentTime = new Date(currentMsg.time).getTime();
const prevTime = new Date(prevMsg.time).getTime();
const diffMinutes = (currentTime - prevTime) / (1000 * 60);
return diffMinutes > 5;
},
// 格式化消息时间(只做格式化,不做多余转换)
formatTime(date) {
if (!date) return "";
// === 严格遵守用户要求后端时间直接去掉T本地时间用toISOString() ===
let str = "";
if (typeof date === "string" && date.includes("T")) {
// 后端返回的时间格式 "2025-06-12T02:22:39"
// 直接使用,不做转换
str = date;
} else if (date instanceof Date) {
// 本地时间使用toISOString()方法
str = date.toISOString();
} else if (typeof date === "number" || /^\d+$/.test(date)) {
// 时间戳格式转换为Date后使用toISOString()
try {
const dateObj = new Date(parseInt(date));
if (!isNaN(dateObj.getTime())) {
str = dateObj.toISOString();
} else {
return String(date);
}
} catch (error) {
return String(date);
}
} else {
// 其他格式尝试转换为Date对象后使用toISOString()
try {
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) {
return String(date);
}
str = dateObj.toISOString();
} catch (error) {
return String(date);
}
}
const [d, t] = str.split("T");
if (!t) return str;
const [hour, minute] = t.split(":");
// 取当前UTC日期
const now = new Date();
const nowUTC = now.toISOString().split("T")[0];
const msgUTC = d;
if (nowUTC === msgUTC) {
return `UTC ${this.$t("chat.today")} ${hour}:${minute}`;
}
// 判断昨天
const yesterdayUTC = new Date(Date.now() - 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0];
if (yesterdayUTC === msgUTC) {
return `UTC ${this.$t("chat.yesterday")} ${hour}:${minute}`;
}
return `UTC ${d} ${hour}:${minute}`;
},
// 格式化最后消息时间(显示年月日和时分)
formatLastTime(date) {
if (!date) {
return ""; // 返回友好的默认值
}
try {
// === 严格遵守用户要求后端时间直接去掉T本地时间用toISOString() ===
if (typeof date === "string" && date.includes("T")) {
// 后端返回的时间格式 "2025-06-12T02:22:39"
// 直接去掉T提取年月日时分不做任何转换
const [datePart, timePart] = date.split("T");
if (datePart && timePart) {
// 提取时分(去掉秒和毫秒)
const timeOnly = timePart.split(":").slice(0, 2).join(":");
// 返回格式YYYY-MM-DD HH:mm UTC
return `UTC ${datePart} ${timeOnly}`;
}
} else if (date instanceof Date) {
// 本地时间使用toISOString()方法
const timeStr = date.toISOString();
const [datePart, timePart] = timeStr.split("T");
if (datePart && timePart) {
const timeOnly = timePart.split(":").slice(0, 2).join(":");
return `UTC ${datePart} ${timeOnly}`;
}
} else if (typeof date === "number" || /^\d+$/.test(date)) {
// 时间戳格式转换为Date后使用toISOString()
const dateObj = new Date(parseInt(date));
if (!isNaN(dateObj.getTime())) {
const timeStr = dateObj.toISOString();
const [datePart, timePart] = timeStr.split("T");
if (datePart && timePart) {
const timeOnly = timePart.split(":").slice(0, 2).join(":");
return `UTC ${datePart} ${timeOnly}`;
}
}
}
// 兜底处理尝试解析为Date对象
const dateObj = new Date(date);
if (!isNaN(dateObj.getTime())) {
const timeStr = dateObj.toISOString();
const [datePart, timePart] = timeStr.split("T");
if (datePart && timePart) {
const timeOnly = timePart.split(":").slice(0, 2).join(":");
return `UTC ${datePart} ${timeOnly}`;
}
}
return String(date); // 如果都解析失败,返回原始值
} catch (error) {
console.error("格式化时间失败:", error);
return String(date);
}
},
// 获取默认头像
getDefaultAvatar(name) {
if (!name) return "";
// 根据名称生成颜色
const colors = [
"#4CAF50",
"#9C27B0",
"#FF5722",
"#2196F3",
"#FFC107",
"#607D8B",
"#E91E63",
];
const index = Math.abs(name.charCodeAt(0)) % colors.length;
const color = colors[index];
// 获取名称首字母
const initial = name.charAt(0).toUpperCase();
return initial;
// 生成占位头像URL
},
// 处理滚动事件
handleScroll() {
const container = this.$refs.messageContainer;
if (!container) return;
this.updateLastActivityTime();
this.showScrollButton = !this.isAtBottom();
if (this.isAtBottom()) {
this.userViewHistory = false;
// 到底部时自动已读
this.markMessagesAsRead(this.currentContactId);
} else {
this.userViewHistory = true;
}
},
// 小时钟加载历史消息(7天前)
async loadHistory() {
this.loadingHistory = true;
this.userViewHistory = true; // 用户主动查看历史
if (!this.currentContactId) return;
try {
this.messagesLoading = true;
// 获取当前已加载的消息列表
const currentMsgs = this.messages[this.currentContactId] || [];
// 检查是否有历史消息记录
if (currentMsgs.length === 0) {
this.$message({
message: this.$t("chat.noMoreHistory") || "没有更多历史消息",
type: "warning",
duration: 3000,
showClose: true,
});
return;
}
// 获取ID最小的消息最早的消息作为查询参数
const earliestMessage = this.getEarliestMessage(currentMsgs);
if (!earliestMessage || !earliestMessage.id) {
this.$message({
message: this.$t("chat.noMoreHistory") || "没有更多历史消息",
type: "warning",
duration: 3000,
showClose: true,
});
return;
}
// 使用最早消息的id作为查询参数
this.history7Params.id = earliestMessage.id;
console.log("🕐 小时钟加载历史消息 - 使用最早消息ID:", {
totalMessages: currentMsgs.length,
earliestMessageId: earliestMessage.id,
earliestMessageTime: earliestMessage.time,
});
this.history7Params.roomId = this.currentContactId;
this.history7Params.email = this.userEmail;
const response = await getHistory7(this.history7Params);
console.log("📡 loadHistory - 小时钟接口响应详情:", {
responseCode: response?.code,
hasData: !!response?.data,
dataLength: response?.data?.length || 0,
currentContactId: this.currentContactId,
requestParams: this.history7Params,
});
if (response && response.code === 200 && response.data) {
console.log("📦 loadHistory - 小时钟原始数据:", response.data);
// 过滤当前聊天室的消息(处理数据类型不匹配问题)
const filteredHistoryMessages = response.data.filter((msg) => {
// 支持字符串和数字类型的roomId比较
return (
msg.roomId == this.currentContactId ||
String(msg.roomId) === String(this.currentContactId)
);
});
console.log("🔍 loadHistory - 小时钟过滤后数据:", {
originalCount: response.data.length,
filteredCount: filteredHistoryMessages.length,
targetRoomId: this.currentContactId,
messageRoomIds: response.data.map((m) => m.roomId).slice(0, 5),
});
let historyMessages = filteredHistoryMessages.map((msg) => ({
id: msg.id,
sender: msg.sendEmail,
avatar: "iconfont icon-icon28",
content: msg.content,
time: msg.createTime, // 直接用字符串
isSelf: msg.isSelf === 1,
isImage: msg.type === 2,
isRead: msg.isRead === 1,
type: msg.type,
roomId: msg.roomId,
}));
console.log("🔄 loadHistory - 小时钟处理后消息:", {
processedCount: historyMessages.length,
messageIds: historyMessages.map((m) => m.id),
messageTimes: historyMessages.map((m) => m.time).slice(0, 3),
});
// 检查重复消息
const currentMessageIds = (
this.messages[this.currentContactId] || []
).map((m) => m.id);
const newHistoryIds = historyMessages.map((m) => m.id);
const duplicateHistoryIds = newHistoryIds.filter((id) =>
currentMessageIds.includes(id)
);
console.log("🔍 loadHistory - 小时钟重复消息检查:", {
currentMessageCount: currentMessageIds.length,
newHistoryCount: newHistoryIds.length,
duplicateCount: duplicateHistoryIds.length,
duplicateIds: duplicateHistoryIds,
});
// 如果没有获取到新的历史消息
if (historyMessages.length === 0) {
console.warn(
"⚠️ loadHistory - 小时钟过滤后无消息,设置无更多历史状态"
);
this.hasMoreHistory = false;
this.noMoreHistoryMessage =
this.$t("chat.noMoreHistory") || "没有更多历史消息";
return;
}
// 过滤掉重复的历史消息
const uniqueHistoryMessages = historyMessages.filter(
(msg) => !currentMessageIds.includes(msg.id)
);
console.log("✂️ loadHistory - 小时钟去重后消息:", {
originalCount: historyMessages.length,
uniqueCount: uniqueHistoryMessages.length,
removedDuplicates:
historyMessages.length - uniqueHistoryMessages.length,
});
// 如果去重后没有新的历史消息
if (uniqueHistoryMessages.length === 0) {
console.warn(
"⚠️ loadHistory - 小时钟去重后无新消息,设置无更多历史状态"
);
this.hasMoreHistory = false;
this.noMoreHistoryMessage =
this.$t("chat.noMoreHistory") || "没有更多历史消息";
return;
}
// 使用去重后的历史消息
historyMessages = uniqueHistoryMessages;
historyMessages = this.sortMessages(historyMessages);
const currentMessages = this.messages[this.currentContactId] || [];
this.$set(this.messages, this.currentContactId, [
...historyMessages,
...currentMessages,
]);
console.log("✅ loadHistory - 小时钟历史消息加载完成:", {
loadedCount: historyMessages.length,
totalCount: this.messages[this.currentContactId].length,
});
// === 移除加载成功提示:消息已显示在聊天中 ===
} else {
console.warn(
"⚠️ loadHistory - 小时钟接口返回无数据,设置无更多历史状态"
);
this.hasMoreHistory = false;
this.noMoreHistoryMessage =
this.$t("chat.noMoreHistory") || "没有更多历史消息";
}
} catch (error) {
console.error("加载历史消息异常:", error);
this.$message({
message:
this.$t("chat.historicalFailure") || "加载历史消息失败,请重试",
type: "error",
duration: 3000,
showClose: true,
});
} finally {
this.messagesLoading = false;
this.loadingHistory = false;
}
},
// 刷新当前聊天消息
async refreshMessages() {
if (!this.currentContactId) return;
await this.loadMessages(this.currentContactId);
},
// 打开图片上传控件
openImageUpload() {
if (!this.currentContact) return;
this.$refs.imageInput.click();
},
// 将本地时间转换为 UTC 时间
convertToUTC(date) {
if (!date) return null;
const d = new Date(date);
return new Date(d.getTime() - d.getTimezoneOffset() * 60000);
},
// 将 UTC 时间转换为本地时间
convertToLocal(utcDate) {
if (!utcDate) return null;
const d = new Date(utcDate);
return new Date(d.getTime() + d.getTimezoneOffset() * 60000);
},
// 修正后端返回不带Z的UTC时间字符串
fixToUTC(str) {
if (typeof str !== "string") return str;
if (str.endsWith("Z") || /[+-]\d{2}:?\d{2}$/.test(str)) return str;
return str + "Z";
},
/**
* 智能消息排序ID为主时间为辅
* 解决快速发送消息时排序混乱的问题
* @param {Array} messages - 消息数组
* @returns {Array} - 排序后的消息数组
*/
sortMessages(messages) {
if (!messages || messages.length <= 1) return messages;
return messages.sort((a, b) => {
// 策略1: 如果两条消息都有ID优先使用ID排序更可靠
if (a.id && b.id) {
const idA = parseInt(a.id);
const idB = parseInt(b.id);
if (!isNaN(idA) && !isNaN(idB)) {
const idDiff = idA - idB;
if (idDiff !== 0) {
return idDiff; // ID不同时直接按ID排序
}
}
}
// 策略2: ID相同或无ID时使用时间排序
const timeA = a.time ? new Date(a.time).getTime() : 0;
const timeB = b.time ? new Date(b.time).getTime() : 0;
return timeA - timeB;
});
},
/**
* 检查消息数组是否需要重新排序
* @param {Array} messages - 消息数组
* @returns {boolean} - 是否需要排序
*/
needsResort(messages) {
if (!messages || messages.length <= 1) return false;
// 检查最后几条消息是否按顺序排列
const checkCount = Math.min(5, messages.length);
const recentMessages = messages.slice(-checkCount);
for (let i = 1; i < recentMessages.length; i++) {
const prev = recentMessages[i - 1];
const curr = recentMessages[i];
// 如果有ID且ID乱序需要重排
if (prev.id && curr.id) {
if (parseInt(prev.id) > parseInt(curr.id)) {
return true;
}
} else {
// 使用时间比较
const prevTime = new Date(prev.time).getTime();
const currTime = new Date(curr.time).getTime();
if (prevTime > currTime) {
return true;
}
}
}
return false;
},
/**
* 获取消息数组中ID最小的消息用于历史消息分页
* 策略优先使用ID最小的消息如果没有有效ID则使用时间最早的消息
* @param {Array} messages - 消息数组
* @returns {Object|null} - ID最小或时间最早的消息对象
*/
getEarliestMessage(messages) {
if (!messages || messages.length === 0) {
console.warn("⚠️ getEarliestMessage: 消息数组为空");
return null;
}
console.log("🔍 查找最早消息:", {
totalCount: messages.length,
messageIds: messages.map((m) => m.id).slice(0, 5), // 显示前5个ID
messageTimes: messages.map((m) => m.time).slice(0, 3), // 显示前3个时间
});
// 策略1: 优先使用有有效ID的消息进行比较
const messagesWithValidId = messages.filter((msg) => {
const id = parseInt(msg.id);
return msg.id && !isNaN(id) && id > 0;
});
if (messagesWithValidId.length > 0) {
// 找到ID最小的消息
const earliestById = messagesWithValidId.reduce((earliest, current) => {
const earliestId = parseInt(earliest.id);
const currentId = parseInt(current.id);
return currentId < earliestId ? current : earliest;
});
console.log("✅ 使用ID最小的消息:", {
messageId: earliestById.id,
messageTime: earliestById.time,
content: earliestById.content?.substring(0, 30) + "...",
});
return earliestById;
}
// 策略2: 如果没有有效ID使用时间最早的消息
console.log("⚠️ 没有有效ID降级使用时间最早的消息");
const earliestByTime = messages.reduce((earliest, current) => {
const earliestTime = new Date(earliest.time || 0).getTime();
const currentTime = new Date(current.time || 0).getTime();
return currentTime < earliestTime ? current : earliest;
});
console.log("✅ 使用时间最早的消息:", {
messageTime: earliestByTime.time,
content: earliestByTime.content?.substring(0, 30) + "...",
hasId: !!earliestByTime.id,
});
return earliestByTime;
},
/**
* 智能联系人排序:时间为主,确保精确度
* @param {Array} contacts - 联系人数组
* @returns {Array} - 排序后的联系人数组
*/
sortContactsByTime(contacts) {
return contacts.sort((a, b) => {
// 1. 首先按重要性排序(重要的在前)
if (a.important && !b.important) return -1;
if (!a.important && b.important) return 1;
// 2. 然后按时间倒序(最新在前)
const aTime = this.parseTimeForSort(a.lastTime);
const bTime = this.parseTimeForSort(b.lastTime);
// 降序排序:最新的在前面
return bTime - aTime;
});
},
/**
* 调试工具:分析历史消息加载问题
* 在浏览器控制台调用this.$refs.customerService.debugHistoryLoading()
*/
debugHistoryLoading() {
console.log("🔍 历史消息加载调试信息:");
console.log(
"当前联系人ID:",
this.currentContactId,
typeof this.currentContactId
);
console.log(
"当前消息数量:",
this.messages[this.currentContactId]?.length || 0
);
console.log("历史消息状态:", {
hasMoreHistory: this.hasMoreHistory,
noMoreHistoryMessage: this.noMoreHistoryMessage,
});
console.log("请求参数:", this.history7Params);
const currentMsgs = this.messages[this.currentContactId] || [];
if (currentMsgs.length > 0) {
const earliestMessage = this.getEarliestMessage(currentMsgs);
console.log("最早消息:", earliestMessage);
console.log(
"消息ID分布:",
currentMsgs.map((m) => ({ id: m.id, time: m.time })).slice(0, 10)
);
}
return {
currentContactId: this.currentContactId,
messageCount: currentMsgs.length,
hasMoreHistory: this.hasMoreHistory,
noMoreHistoryMessage: this.noMoreHistoryMessage,
requestParams: this.history7Params,
earliestMessage: this.getEarliestMessage(currentMsgs),
};
},
/**
* 获取未读消息的localStorage键名
*/
getUnreadStorageKey(roomId) {
return `cs_unread_${roomId}`;
},
/**
* 读取未读数
*/
getUnreadCount(roomId) {
const key = this.getUnreadStorageKey(roomId);
return parseInt(localStorage.getItem(key), 10) || 0;
},
/**
* 更新未读数
*/
setUnreadCount(roomId, count) {
const key = this.getUnreadStorageKey(roomId);
localStorage.setItem(key, String(count));
},
/**
* 监听storage事件实现多窗口未读同步
*/
handleStorageChange(e) {
if (e.key && e.key.startsWith("cs_unread_")) {
const roomId = e.key.replace("cs_unread_", "");
const count = parseInt(e.newValue, 10) || 0;
const contact = this.contacts.find((c) => c.roomId == roomId);
if (contact) contact.unread = count;
}
},
// 处理网络状态变化
handleNetworkChange() {
this.networkStatus = navigator.onLine ? "online" : "offline";
if (navigator.onLine) {
//网络恢复重新刷新页面
location.reload(); // 重新加载当前页面
}
},
},
beforeDestroy() {
if (this.stompClient) {
if (this.stompClient.connected) {
this.stompClient.disconnect(() => {
console.log("WebSocket 已断开连接");
});
}
}
// 组件销毁前断开连接
this.disconnectWebSocket();
// 移除滚动事件监听
if (this.$refs.messageContainer) {
this.$refs.messageContainer.removeEventListener(
"scroll",
this.handleScroll
);
}
// 清理新增的资源
if (this.visibilityHandler) {
document.removeEventListener("visibilitychange", this.visibilityHandler);
}
if (this.activityCheckInterval) {
clearInterval(this.activityCheckInterval);
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
// === 新增:清理活动监听器 ===
if (this.activityEvents && this.activityHandler) {
this.activityEvents.forEach((event) => {
document.removeEventListener(event, this.activityHandler, true);
});
}
// === 新增:清除连接验证定时器 ===
this.clearConnectionVerifyTimer();
// === 新增:清除心跳和连接检查定时器 ===
this.stopHeartbeat();
this.stopConnectionCheck();
window.removeEventListener("storage", this.handleStorageChange);
// 移除新添加的事件监听
window.removeEventListener("online", this.handleNetworkChange);
window.removeEventListener("offline", this.handleNetworkChange);
},
};
</script>
<style scoped>
.cs-chat-container {
width: 65%;
height: 600px;
margin: 0 auto;
background-color: #f5f6f7;
border-radius: 10px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-top: 50px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
position: relative;
}
.cs-chat-wrapper {
display: flex;
height: 100%;
}
/* 联系人列表样式 */
.cs-contact-list {
width: 290px;
min-width: 260px; /* 添加最小宽度 */
border-right: 1px solid #e0e0e0;
background-color: #fff;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden; /* 防止整体出现滚动条 */
}
.cs-header {
padding: 15px;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
color: #333;
font-size: 16px;
background-color: #f8f8f8;
}
.cs-search {
padding: 10px;
border-bottom: 1px solid #e0e0e0;
}
.cs-contacts {
flex: 1;
overflow-y: auto;
}
.cs-contact-item {
display: flex;
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.cs-contact-item:hover {
background-color: #f5f5f5;
}
.cs-contact-item.active {
background-color: #e6f7ff;
}
/* 修改头像区域样式 */
.cs-avatar {
position: relative;
margin-right: 10px;
flex-shrink: 0; /* 防止头像被压缩 */
}
.unread-badge {
position: absolute;
top: -5px;
right: -5px;
background-color: #f56c6c;
color: white;
font-size: 12px;
min-width: 16px;
height: 16px;
text-align: center;
line-height: 16px;
border-radius: 8px;
padding: 0 4px;
}
.cs-contact-info {
flex: 1;
min-width: 0; /* 允许内容收缩 */
overflow: hidden; /* 防止内容溢出 */
}
.cs-contact-name {
font-weight: 500;
font-size: 14px;
color: #333;
display: flex;
justify-content: space-between;
margin-bottom: 4px;
white-space: nowrap; /* 防止换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 显示省略号 */
}
.cs-contact-time {
font-size: 12px;
color: #999;
font-weight: normal;
}
.cs-contact-msg {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%; /* 限制最大宽度 */
}
.important-tag {
color: #f56c6c;
font-size: 12px;
margin-right: 4px;
}
/* 聊天区域样式 */
.cs-chat-area {
flex: 1;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.cs-chat-header {
padding: 15px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
}
.cs-chat-title {
font-weight: bold;
font-size: 16px;
color: #333;
display: flex;
align-items: center;
gap: 10px;
}
.cs-header-actions i {
font-size: 18px;
margin-left: 15px;
color: #666;
cursor: pointer;
}
.cs-header-actions i:hover {
color: #409eff;
}
.cs-chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
background-color: #f5f5f5;
height: calc(100% - 180px); /* 减去头部和输入框的高度 */
position: relative; /* 添加相对定位 */
}
.cs-loading,
.cs-empty-chat {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #909399;
}
.cs-loading i,
.cs-empty-chat i {
font-size: 60px;
margin-bottom: 20px;
color: #dcdfe6;
}
/* 确保消息列表正确显示 */
.cs-message-list {
display: flex;
flex-direction: column;
padding-bottom: 20px; /* 添加底部间距 */
}
.cs-message {
margin-bottom: 15px;
}
.cs-message-time {
text-align: center;
margin: 10px 0;
font-size: 12px;
color: #909399;
}
.cs-message-content {
display: flex;
align-items: flex-start;
}
.cs-message-self .cs-message-content {
flex-direction: row-reverse;
}
.cs-message-self .cs-avatar {
margin-right: 0;
margin-left: 10px;
}
/* 调整消息气泡样式 */
.cs-bubble {
max-width: 70%;
padding: 8px 12px;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
position: relative;
margin-bottom: 4px; /* 添加消息间距 */
}
.cs-message-self .cs-bubble {
background-color: #d8f4fe;
}
.cs-sender {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.cs-text {
font-size: 14px;
line-height: 1.5;
word-break: break-word;
}
/* 确保图片消息正确显示 */
.cs-image img {
max-width: 200px;
max-height: 200px;
border-radius: 4px;
cursor: pointer;
display: block; /* 确保图片正确显示 */
}
/* 输入区域样式 */
.cs-chat-input {
padding: 10px;
background-color: #fff;
border-top: 1px solid #e0e0e0;
}
.cs-toolbar {
padding: 5px 0;
margin-bottom: 5px;
}
.cs-toolbar i {
font-size: 18px;
margin-right: 15px;
color: #606266;
cursor: pointer;
}
.cs-toolbar i:hover {
color: #409eff;
}
.cs-input-area {
margin-bottom: 10px;
}
.cs-send-area {
display: flex;
justify-content: flex-end;
align-items: center;
}
.cs-counter {
margin-right: 10px;
font-size: 12px;
color: #909399;
}
.shop-type {
font-size: 12px;
color: #909399;
font-weight: normal;
margin-left: 5px;
}
/* 图片预览 */
.image-preview-dialog {
display: flex;
justify-content: center;
align-items: center;
}
.preview-image {
max-width: 100%;
max-height: 80vh;
}
/* 响应式调整 */
@media (max-width: 768px) {
.cs-contact-list {
width: 200px;
}
.cs-image img {
max-width: 150px;
max-height: 150px;
}
}
/* 重要标记图标样式 */
.important-star {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
margin-left: 5px;
color: #c0c4cc;
transition: color 0.3s;
flex-shrink: 0; /* 防止图标被压缩 */
}
.important-star:hover {
color: #ac85e0; /* 鼠标悬停时的颜色 */
}
.important-star.is-important {
color: #ac85e0; /* 重要标记时的紫色 */
}
.important-star i {
font-size: 16px;
}
/* 调整联系人项目布局 */
.cs-contact-item {
display: flex;
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
align-items: center;
width: 100%;
box-sizing: border-box; /* 确保padding不会导致溢出 */
}
/* 重要标签的样式调整 */
.important-tag {
color: #ac85e0; /* 紫色标签 */
font-size: 12px;
margin-right: 4px;
font-weight: bold;
}
/* 游客标记样式 */
.guest-badge {
position: absolute;
bottom: -5px;
right: -5px;
background-color: #67c23a;
color: white;
font-size: 10px;
width: 16px;
height: 16px;
text-align: center;
line-height: 16px;
border-radius: 8px;
}
/* 游客聊天室项的背景稍微区分 */
.cs-contact-item.is-guest {
background-color: #f9f9f9;
}
/* 滚动按钮样式 */
.scroll-to-bottom {
position: absolute; /* 改为 fixed 定位 */
right: 4px; /* 根据容器宽度调整位置 */
bottom: 184px;
background-color: #fff;
border-radius: 5px 0px 0px 5px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
z-index: 1000; /* 确保按钮在最上层 */
padding: 5px 1vw;
font-size: 0.7vw;
color: #7638ff;
}
.scroll-to-bottom:hover {
background-color: #f0f0f0;
transform: translateY(-2px);
}
.scroll-to-bottom i {
font-size: 0.8vw;
color: #7638ff;
margin-left: 5px;
}
/* 添加连接状态样式 */
.connection-status {
position: fixed;
top: 8%;
right: 41%;
padding: 8px 16px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
z-index: 1000;
color: #7638ff;
/* box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); */
}
.connection-status.error {
background-color: #fef0f0;
color: #f56c6c;
}
.connection-status.connecting {
background-color: #f0f9eb;
color: #67c23a;
}
/* 历史消息区域样式 */
.history-section {
margin-bottom: 10px;
}
.history-indicator {
transition: all 0.3s ease;
border-radius: 4px;
padding: 8px 12px;
}
/* .history-indicator:hover {
background-color: #f0f9ff;
color: #1890ff;
transform: translateY(-1px);
} */
.no-more-history {
/* background-color: #fafafa; */
border-radius: 4px;
/* border: 1px dashed #d9d9d9; */
transition: all 0.3s ease;
}
.no-more-history i {
margin-right: 6px;
font-size: 0.8em;
}
.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;
}
/* .no-more-history:hover {
border-color: #b7b7b7;
background-color: #f5f5f5;
} */
</style>