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