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