2025-04-22 06:26:41 +00:00
|
|
|
|
<template>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<div class="chat-widget">
|
2025-05-23 06:46:29 +00:00
|
|
|
|
<!-- 添加网络状态提示 -->
|
|
|
|
|
<div v-if="networkStatus === 'offline'" class="network-status">
|
|
|
|
|
<i class="el-icon-warning"></i>
|
|
|
|
|
<span>{{ $t("chat.networkError") || "网络连接已断开" }}</span>
|
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<!-- 聊天图标 -->
|
|
|
|
|
<div
|
|
|
|
|
class="chat-icon"
|
|
|
|
|
@click="toggleChat"
|
|
|
|
|
:class="{ active: isChatOpen }"
|
2025-05-23 06:46:29 +00:00
|
|
|
|
:aria-label="$t('chat.openCustomerService') || '打开客服聊天'"
|
2025-04-25 06:09:32 +00:00
|
|
|
|
tabindex="0"
|
|
|
|
|
@keydown.enter="toggleChat"
|
|
|
|
|
@keydown.space="toggleChat"
|
|
|
|
|
>
|
|
|
|
|
<i class="el-icon-chat-dot-round"></i>
|
|
|
|
|
<span v-if="unreadMessages > 0" class="unread-badge">{{
|
|
|
|
|
unreadMessages
|
|
|
|
|
}}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 聊天对话框 -->
|
|
|
|
|
<transition name="chat-slide">
|
|
|
|
|
<div v-show="isChatOpen" class="chat-dialog">
|
|
|
|
|
<div class="chat-header">
|
|
|
|
|
<div class="chat-title">{{ $t("chat.title") || "在线客服" }}</div>
|
|
|
|
|
<div class="chat-actions">
|
|
|
|
|
<i class="el-icon-minus" @click="minimizeChat"></i>
|
|
|
|
|
<i class="el-icon-close" @click="closeChat"></i>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chat-body" ref="chatBody">
|
2025-05-28 07:01:22 +00:00
|
|
|
|
<!-- 游客提示信息 -->
|
|
|
|
|
<div v-if="userType === 0" class="guest-notice">
|
|
|
|
|
<span class="guest-notice-content">
|
|
|
|
|
<i class="el-icon-info"></i>
|
|
|
|
|
|
|
|
|
|
<span
|
|
|
|
|
>{{ $t("chat.guestNotice") || "游客模式下聊天记录不会保存," }}
|
|
|
|
|
<a @click="handleLoginClick" class="login-link">{{
|
|
|
|
|
$t("chat.loginToSave") || "登录"
|
|
|
|
|
}}</a>
|
|
|
|
|
{{ $t("chat.guestNotice2") || "后即可保存" }}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<!-- 连接状态提示 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="connectionStatus === 'connecting'"
|
|
|
|
|
class="chat-status connecting"
|
|
|
|
|
>
|
|
|
|
|
<i class="el-icon-loading"></i>
|
2025-05-28 07:01:22 +00:00
|
|
|
|
<p>
|
|
|
|
|
{{ $t("chat.connectToCustomerService") || "正在连接客服系统..." }}
|
|
|
|
|
</p>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<div
|
|
|
|
|
v-else-if="connectionStatus === 'error'"
|
|
|
|
|
class="chat-status error"
|
|
|
|
|
>
|
|
|
|
|
<i class="el-icon-warning"></i>
|
2025-05-28 07:01:22 +00:00
|
|
|
|
<p>
|
|
|
|
|
{{
|
|
|
|
|
connectionError ||
|
|
|
|
|
$t("chat.connectionFailed") ||
|
|
|
|
|
"连接失败,请稍后重试"
|
|
|
|
|
}}
|
|
|
|
|
</p>
|
|
|
|
|
<div class="error-actions">
|
|
|
|
|
<button @click="handleRetryConnect" class="retry-button">
|
|
|
|
|
{{ $t("chat.tryConnectingAgain") || "重试连接" }}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
v-if="showRefreshButton"
|
|
|
|
|
@click="refreshPage"
|
|
|
|
|
class="refresh-button"
|
|
|
|
|
>
|
|
|
|
|
{{ $t("chat.refreshPage") || "刷新页面" }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 消息列表 -->
|
|
|
|
|
<template v-else>
|
|
|
|
|
<!-- 历史消息加载提示 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="hasMoreHistory && messages.length > 0"
|
|
|
|
|
class="history-indicator"
|
2025-05-28 07:01:22 +00:00
|
|
|
|
:class="{ 'no-more': !hasMoreHistory }"
|
|
|
|
|
@click.stop="loadMoreHistory"
|
2025-04-25 06:09:32 +00:00
|
|
|
|
>
|
|
|
|
|
<i class="el-icon-arrow-up"></i>
|
|
|
|
|
<span>{{
|
2025-05-28 07:01:22 +00:00
|
|
|
|
isLoadingHistory
|
|
|
|
|
? $t("chat.loading") || "加载中..."
|
|
|
|
|
: hasMoreHistory
|
|
|
|
|
? $t("chat.loadMore") || "加载更多历史消息"
|
|
|
|
|
: $t("chat.noMoreHistory") || "没有更多历史消息了"
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}}</span>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
<!-- 没有消息时的欢迎提示 -->
|
2025-05-28 07:01:22 +00:00
|
|
|
|
<div
|
|
|
|
|
v-if="messages.length === 0 && userType !== 0"
|
|
|
|
|
class="chat-empty"
|
|
|
|
|
>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
{{
|
|
|
|
|
$t("chat.welcome") || "欢迎使用在线客服,请问有什么可以帮您?"
|
|
|
|
|
}}
|
2025-04-22 06:26:41 +00:00
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
<!-- 消息项 -->
|
|
|
|
|
<div
|
|
|
|
|
v-for="(msg, index) in messages"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="chat-message"
|
|
|
|
|
:class="{
|
|
|
|
|
'chat-message-user': msg.type === 'user',
|
|
|
|
|
'chat-message-system': msg.type === 'system',
|
|
|
|
|
'chat-message-loading': msg.isLoading,
|
|
|
|
|
'chat-message-hint': msg.isSystemHint,
|
|
|
|
|
'chat-message-history': msg.isHistory,
|
|
|
|
|
}"
|
|
|
|
|
>
|
|
|
|
|
<!-- 系统提示消息,如加载中、无更多消息等 -->
|
|
|
|
|
<div v-if="msg.isLoading || msg.isSystemHint" class="system-hint">
|
|
|
|
|
<i v-if="msg.isLoading" class="el-icon-loading"></i>
|
|
|
|
|
<span>{{ msg.text }}</span>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
<!-- 普通消息 -->
|
|
|
|
|
<template v-else>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
<div class="message-avatar">
|
|
|
|
|
<i v-if="msg.type === 'system'" class="el-icon-service"></i>
|
|
|
|
|
<i v-else class="el-icon-user"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="message-content">
|
2025-05-30 08:39:09 +00:00
|
|
|
|
<!-- 时间显示在右上角 -->
|
|
|
|
|
<!-- <span class="message-time">{{ formatTime(msg.time) }}</span> -->
|
2025-04-22 06:26:41 +00:00
|
|
|
|
<!-- 文本消息 -->
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<div v-if="!msg.isImage" class="message-text">
|
|
|
|
|
{{ msg.text }}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-04-22 06:26:41 +00:00
|
|
|
|
<!-- 图片消息 -->
|
|
|
|
|
<div v-else class="message-image">
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<img
|
|
|
|
|
:src="msg.imageUrl"
|
|
|
|
|
@click="previewImage(msg.imageUrl)"
|
2025-05-23 06:46:29 +00:00
|
|
|
|
:alt="$t('chat.picture') || '聊天图片'"
|
2025-05-28 07:01:22 +00:00
|
|
|
|
@load="handleImageLoad"
|
2025-04-25 06:09:32 +00:00
|
|
|
|
/>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
<div class="message-footer">
|
2025-05-30 08:39:09 +00:00
|
|
|
|
<!-- <span class="message-time">{{ formatTime(msg.time) }}</span> -->
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<!-- 添加已读状态显示 -->
|
2025-04-30 07:22:35 +00:00
|
|
|
|
<span
|
|
|
|
|
v-if="msg.type === 'user'"
|
|
|
|
|
class="message-read-status"
|
|
|
|
|
>
|
2025-05-28 07:01:22 +00:00
|
|
|
|
{{
|
|
|
|
|
msg.isRead
|
|
|
|
|
? $t("chat.read") || "已读"
|
|
|
|
|
: $t("chat.unread") || "未读"
|
|
|
|
|
}}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
</span>
|
2025-04-30 07:22:35 +00:00
|
|
|
|
</div>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
</template>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="chat-footer">
|
|
|
|
|
<div class="chat-toolbar">
|
|
|
|
|
<label
|
|
|
|
|
for="imageUpload"
|
|
|
|
|
class="image-upload-label"
|
|
|
|
|
:class="{ disabled: connectionStatus !== 'connected' }"
|
|
|
|
|
>
|
|
|
|
|
<i class="el-icon-picture-outline"></i>
|
|
|
|
|
</label>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
<input
|
2025-04-25 06:09:32 +00:00
|
|
|
|
type="file"
|
|
|
|
|
id="imageUpload"
|
|
|
|
|
ref="imageUpload"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
@change="handleImageUpload"
|
|
|
|
|
style="display: none"
|
2025-04-22 06:26:41 +00:00
|
|
|
|
:disabled="connectionStatus !== 'connected'"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-05-30 08:39:09 +00:00
|
|
|
|
<div class="chat-input-wrapper" style="display: flex;align-items: center;">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
class="chat-input"
|
|
|
|
|
v-model="inputMessage"
|
|
|
|
|
:maxlength="maxMessageLength"
|
|
|
|
|
@input="handleInputMessage"
|
|
|
|
|
:placeholder="$t('chat.inputPlaceholder') || '请输入您的问题...'"
|
|
|
|
|
:disabled="connectionStatus !== 'connected'"
|
|
|
|
|
/>
|
|
|
|
|
<!-- <span class="input-counter">{{ maxMessageLength - inputMessage.length }}</span> -->
|
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<button
|
|
|
|
|
class="chat-send"
|
|
|
|
|
@click="sendMessage"
|
|
|
|
|
:disabled="connectionStatus !== 'connected' || !inputMessage.trim()"
|
|
|
|
|
>
|
|
|
|
|
{{ $t("chat.send") || "发送" }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 图片预览 -->
|
|
|
|
|
<div
|
|
|
|
|
v-if="showImagePreview"
|
|
|
|
|
class="image-preview-overlay"
|
|
|
|
|
@click="closeImagePreview"
|
|
|
|
|
>
|
|
|
|
|
<div class="image-preview-container">
|
|
|
|
|
<img :src="previewImageUrl" class="preview-image" />
|
|
|
|
|
<i
|
|
|
|
|
class="el-icon-close preview-close"
|
|
|
|
|
@click="closeImagePreview"
|
|
|
|
|
></i>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-04-25 06:09:32 +00:00
|
|
|
|
</div>
|
|
|
|
|
</transition>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<script>
|
2025-04-30 07:22:35 +00:00
|
|
|
|
import { Client, Stomp } from "@stomp/stompjs";
|
|
|
|
|
import {
|
|
|
|
|
getUserid,
|
|
|
|
|
getHistory,
|
|
|
|
|
getHistory7,
|
|
|
|
|
getReadMessage,
|
|
|
|
|
getFileUpdate,
|
|
|
|
|
} from "../api/customerService";
|
2025-04-25 06:09:32 +00:00
|
|
|
|
export default {
|
|
|
|
|
name: "ChatWidget",
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
isChatOpen: false,
|
|
|
|
|
inputMessage: "",
|
|
|
|
|
messages: [],
|
|
|
|
|
unreadMessages: 0,
|
|
|
|
|
// 图片预览相关
|
|
|
|
|
showImagePreview: false,
|
|
|
|
|
previewImageUrl: "",
|
|
|
|
|
// WebSocket 相关
|
|
|
|
|
stompClient: null,
|
|
|
|
|
receivingEmail: "",
|
|
|
|
|
connectionStatus: "disconnected", // disconnected, connecting, connected, error
|
|
|
|
|
userType: 0, // 0 游客 1 登录用户 2 客服
|
|
|
|
|
userEmail: "", // 用户标识
|
|
|
|
|
// 自动回复配置
|
|
|
|
|
autoResponses: {
|
|
|
|
|
hello: "您好,有什么可以帮助您的?",
|
|
|
|
|
你好: "您好,有什么可以帮助您的?",
|
|
|
|
|
hi: "您好,有什么可以帮助您的?",
|
|
|
|
|
挖矿: "您可以查看我们的挖矿教程,或者直接创建矿工账户开始挖矿。",
|
|
|
|
|
算力: "您可以在首页查看当前的矿池算力和您的个人算力。",
|
|
|
|
|
收益: "收益根据您的算力贡献按比例分配,详情可以查看收益计算器。",
|
|
|
|
|
帮助: "您可以查看我们的帮助文档,或者提交工单咨询具体问题。",
|
2025-04-22 06:26:41 +00:00
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
isLoadingHistory: false, // 是否正在加载历史消息
|
|
|
|
|
hasMoreHistory: true, // 是否还有更多历史消息
|
|
|
|
|
roomId: "",
|
2025-05-28 07:01:22 +00:00
|
|
|
|
isWebSocketConnected: false,
|
|
|
|
|
heartbeatInterval: null,
|
|
|
|
|
lastHeartbeatTime: null,
|
|
|
|
|
heartbeatTimeout: 120000, // 增加到120秒(2分钟)没有心跳就认为连接断开
|
2025-04-30 07:22:35 +00:00
|
|
|
|
cachedMessages: {}, // 缓存各聊天室的消息
|
|
|
|
|
isMinimized: false, // 区分最小化和关闭状态
|
2025-05-16 06:01:38 +00:00
|
|
|
|
reconnectAttempts: 0,
|
2025-05-28 07:01:22 +00:00
|
|
|
|
maxReconnectAttempts: 3, // 减少最大重连次数
|
|
|
|
|
reconnectInterval: 3000, // 减少重连间隔
|
2025-05-16 06:01:38 +00:00
|
|
|
|
isReconnecting: false,
|
2025-05-23 06:46:29 +00:00
|
|
|
|
lastActivityTime: Date.now(),
|
|
|
|
|
activityCheckInterval: null,
|
|
|
|
|
networkStatus: "online",
|
|
|
|
|
reconnectTimer: null,
|
2025-05-28 07:01:22 +00:00
|
|
|
|
connectionError: null, // 添加错误信息存储
|
|
|
|
|
showRefreshButton: false, // 添加刷新按钮
|
|
|
|
|
heartbeatCheckInterval: 30000, // 每30秒检查一次心跳
|
2025-05-30 08:39:09 +00:00
|
|
|
|
|
|
|
|
|
maxMessageLength: 300,
|
2025-04-25 06:09:32 +00:00
|
|
|
|
};
|
|
|
|
|
},
|
2025-05-16 06:01:38 +00:00
|
|
|
|
|
2025-04-30 07:22:35 +00:00
|
|
|
|
async created() {
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.determineUserType();
|
2025-04-30 07:22:35 +00:00
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
mounted() {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 添加页面卸载事件监听
|
|
|
|
|
window.addEventListener("beforeunload", this.handleBeforeUnload);
|
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
document.addEventListener("click", this.handleClickOutside);
|
|
|
|
|
// 添加聊天窗口滚动监听
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
if (this.$refs.chatBody) {
|
|
|
|
|
this.$refs.chatBody.addEventListener("scroll", this.handleChatScroll);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 添加页面可见性变化监听
|
|
|
|
|
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 添加网络状态变化监听
|
|
|
|
|
window.addEventListener("online", this.handleNetworkChange);
|
|
|
|
|
window.addEventListener("offline", this.handleNetworkChange);
|
|
|
|
|
|
|
|
|
|
// 添加用户活动检测
|
|
|
|
|
this.startActivityCheck();
|
|
|
|
|
|
|
|
|
|
// 添加用户活动监听
|
|
|
|
|
document.addEventListener("mousemove", this.updateLastActivityTime);
|
|
|
|
|
document.addEventListener("keydown", this.updateLastActivityTime);
|
|
|
|
|
document.addEventListener("click", this.updateLastActivityTime);
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
|
|
|
|
methods: {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 初始化聊天系统
|
|
|
|
|
async initChatSystem() {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
console.log("this.userEmail", this.userEmail);
|
|
|
|
|
|
|
|
|
|
if (!this.userEmail) return;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
try {
|
|
|
|
|
// 获取用户ID和未读消息数
|
2025-05-16 06:01:38 +00:00
|
|
|
|
const userData = await this.fetchUserid({ email: this.userEmail });
|
2025-04-30 07:22:35 +00:00
|
|
|
|
if (userData) {
|
|
|
|
|
this.roomId = userData.id;
|
|
|
|
|
this.receivingEmail = userData.userEmail;
|
|
|
|
|
this.unreadMessages = userData.clientReadNum || 0;
|
|
|
|
|
|
|
|
|
|
// 初始化 WebSocket 连接
|
|
|
|
|
if (!this.isWebSocketConnected) {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.initWebSocket(userData.selfEmail);
|
2025-04-30 07:22:35 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("初始化聊天系统失败:", error);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 初始化 WebSocket 连接
|
2025-05-28 07:01:22 +00:00
|
|
|
|
initWebSocket(selfEmail) {
|
|
|
|
|
// this.determineUserType();
|
|
|
|
|
this.connectWebSocket(selfEmail);
|
2025-04-30 07:22:35 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 确定用户类型和邮箱
|
|
|
|
|
determineUserType() {
|
|
|
|
|
try {
|
|
|
|
|
const token = localStorage.getItem("token");
|
2025-05-16 06:01:38 +00:00
|
|
|
|
console.log("token", token);
|
2025-04-30 07:22:35 +00:00
|
|
|
|
if (!token) {
|
|
|
|
|
// 游客身份
|
|
|
|
|
this.userType = 0;
|
|
|
|
|
this.userEmail = `guest_${Date.now()}_${Math.random()
|
|
|
|
|
.toString(36)
|
|
|
|
|
.substr(2, 9)}`;
|
|
|
|
|
console.log("游客用户:", this.userEmail);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 页面加载时立即获取用户信息
|
|
|
|
|
this.initChatSystem();
|
2025-04-30 07:22:35 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const userInfo = JSON.parse(
|
|
|
|
|
localStorage.getItem("jurisdiction") || "{}"
|
|
|
|
|
);
|
|
|
|
|
const email = JSON.parse(localStorage.getItem("userEmail") || "{}");
|
|
|
|
|
|
|
|
|
|
if (userInfo.roleKey === "customer_service") {
|
|
|
|
|
// 客服用户
|
|
|
|
|
this.userType = 2;
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.userEmail = "";
|
2025-04-30 07:22:35 +00:00
|
|
|
|
} else {
|
|
|
|
|
// 登录用户
|
|
|
|
|
this.userType = 1;
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.userEmail = email;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
}
|
2025-05-28 07:01:22 +00:00
|
|
|
|
|
|
|
|
|
// 页面加载时立即获取用户信息
|
|
|
|
|
this.initChatSystem();
|
2025-04-30 07:22:35 +00:00
|
|
|
|
} catch (parseError) {
|
|
|
|
|
console.error("解析用户信息失败:", parseError);
|
|
|
|
|
// 解析失败时默认为游客
|
|
|
|
|
this.userType = 0;
|
|
|
|
|
this.userEmail = `guest_${Date.now()}_${Math.random()
|
|
|
|
|
.toString(36)
|
|
|
|
|
.substr(2, 9)}`;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("获取用户信息失败:", error);
|
|
|
|
|
// 出错时默认为游客
|
|
|
|
|
this.userType = 0;
|
|
|
|
|
this.userEmail = `guest_${Date.now()}_${Math.random()
|
|
|
|
|
.toString(36)
|
|
|
|
|
.substr(2, 9)}`;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 添加订阅消息的方法
|
2025-05-28 07:01:22 +00:00
|
|
|
|
subscribeToPersonalMessages(selfEmail) {
|
2025-05-16 06:01:38 +00:00
|
|
|
|
if (!this.stompClient || !this.isWebSocketConnected) return;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
try {
|
|
|
|
|
// 订阅个人消息频道
|
|
|
|
|
this.stompClient.subscribe(
|
2025-05-28 07:01:22 +00:00
|
|
|
|
`/sub/queue/user/${selfEmail}`,
|
|
|
|
|
(message) => {
|
|
|
|
|
// 更新最后心跳时间
|
|
|
|
|
this.lastHeartbeatTime = Date.now();
|
|
|
|
|
// 处理消息
|
|
|
|
|
this.onMessageReceived(message);
|
|
|
|
|
}
|
2025-05-16 06:01:38 +00:00
|
|
|
|
);
|
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
console.log("成功订阅消息频道:", `/sub/queue/user/${selfEmail}`);
|
2025-05-16 06:01:38 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("订阅消息失败:", error);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message({
|
|
|
|
|
message:
|
|
|
|
|
this.$t("chat.subscriptionFailed") ||
|
|
|
|
|
"消息订阅失败,可能无法接收新消息",
|
|
|
|
|
type: "error",
|
|
|
|
|
});
|
2025-05-16 06:01:38 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
|
|
|
|
// 连接 WebSocket
|
2025-05-28 07:01:22 +00:00
|
|
|
|
connectWebSocket(selfEmail) {
|
|
|
|
|
if (!selfEmail) selfEmail = this.userEmail;
|
|
|
|
|
if (!selfEmail) {
|
|
|
|
|
this.determineUserType();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-05-16 06:01:38 +00:00
|
|
|
|
if (this.isWebSocketConnected || this.isReconnecting) return;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.connectionStatus = "connecting";
|
|
|
|
|
this.isReconnecting = true;
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.connectionError = null;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
try {
|
|
|
|
|
const wsUrl = `${process.env.VUE_APP_BASE_API}chat/ws`;
|
|
|
|
|
this.stompClient = Stomp.client(wsUrl);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.stompClient.splitLargeFrames = true;
|
2025-05-16 06:01:38 +00:00
|
|
|
|
const headers = {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
email: selfEmail,
|
2025-05-16 06:01:38 +00:00
|
|
|
|
type: this.userType,
|
|
|
|
|
};
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 修改错误处理
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.stompClient.onStompError = (frame) => {
|
|
|
|
|
console.error("STOMP 错误:", frame);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.connectionError = frame.headers.message;
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.handleDisconnect();
|
|
|
|
|
};
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
this.stompClient.connect(
|
|
|
|
|
headers,
|
|
|
|
|
(frame) => {
|
|
|
|
|
console.log("WebSocket Connected:", frame);
|
|
|
|
|
this.isWebSocketConnected = true;
|
|
|
|
|
this.connectionStatus = "connected";
|
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
this.isReconnecting = false;
|
|
|
|
|
this.connectionError = null;
|
|
|
|
|
this.subscribeToPersonalMessages(selfEmail);
|
|
|
|
|
this.startHeartbeat();
|
|
|
|
|
resolve(frame);
|
|
|
|
|
},
|
|
|
|
|
(error) => {
|
|
|
|
|
console.error("WebSocket Error:", error);
|
|
|
|
|
if (error.message.includes("503")) {
|
|
|
|
|
this.connectionError = this.$t("chat.server500");//服务器暂时不可用,请稍后重试
|
|
|
|
|
} else if (error.message.includes("handshake")) {
|
|
|
|
|
this.connectionError = this.$t("chat.CheckNetwork");//"连接失败,请检查网络后重试"
|
|
|
|
|
} else {
|
|
|
|
|
this.connectionError = error.message || this.$t("chat.connectionFailed"); // "连接失败,请刷新页面重试";
|
|
|
|
|
}
|
|
|
|
|
this.isReconnecting = false;
|
|
|
|
|
this.handleDisconnect();
|
|
|
|
|
reject(error);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
// 设置 STOMP 心跳
|
|
|
|
|
this.stompClient.heartbeat.outgoing = 30000; // 30秒发送一次心跳
|
|
|
|
|
this.stompClient.heartbeat.incoming = 30000; // 30秒接收一次心跳
|
|
|
|
|
});
|
2025-05-16 06:01:38 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("初始化 WebSocket 失败:", error);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.connectionError = this.$t("chat.initializationFailed");//"初始化连接失败,请刷新页面重试";
|
|
|
|
|
this.isReconnecting = false;
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.handleDisconnect();
|
2025-05-28 07:01:22 +00:00
|
|
|
|
return Promise.reject(error);
|
2025-05-16 06:01:38 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 添加新重连最多重连5次
|
|
|
|
|
handleDisconnect() {
|
2025-05-23 06:46:29 +00:00
|
|
|
|
if (this.isReconnecting) return;
|
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.isWebSocketConnected = false;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
this.connectionStatus = "error";
|
2025-05-23 06:46:29 +00:00
|
|
|
|
this.isReconnecting = true;
|
2025-05-16 06:01:38 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 清除之前的重连定时器
|
|
|
|
|
if (this.reconnectTimer) {
|
|
|
|
|
clearTimeout(this.reconnectTimer);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// === 新增:统一处理连接数上限错误 ===
|
|
|
|
|
if (
|
|
|
|
|
this.handleConnectionError({
|
|
|
|
|
code: this.connectionError?.code,
|
|
|
|
|
error: this.connectionError?.error,
|
|
|
|
|
message: this.connectionError
|
|
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// === 新增结束 ===
|
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 使用现有的重连逻辑
|
2025-05-16 06:01:38 +00:00
|
|
|
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
|
|
|
this.reconnectAttempts++;
|
|
|
|
|
console.log(
|
|
|
|
|
`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 其余错误类型提示
|
|
|
|
|
let retryMessage = "";
|
|
|
|
|
if (this.connectionError && this.connectionError.includes("503")) {
|
|
|
|
|
retryMessage =this.$t(`chat.server500`);//服务器暂时不可用,正在重试
|
|
|
|
|
} else if (
|
|
|
|
|
this.connectionError &&
|
|
|
|
|
this.connectionError.includes("handshake")
|
|
|
|
|
) {
|
|
|
|
|
retryMessage =this.$t(`chat.connectionFailed`);//连接失败,请刷新页面重试
|
|
|
|
|
} else {
|
|
|
|
|
retryMessage = `${this.$t("chat.break")},${
|
|
|
|
|
this.reconnectInterval / 1000
|
|
|
|
|
}${this.$t("chat.retry")}...`;
|
|
|
|
|
}
|
2025-05-23 06:46:29 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message.warning(retryMessage);
|
|
|
|
|
|
|
|
|
|
this.$message({
|
|
|
|
|
message: retryMessage,
|
|
|
|
|
type: "warning",
|
|
|
|
|
duration: 3000,
|
|
|
|
|
showClose: true,
|
|
|
|
|
});
|
2025-05-23 06:46:29 +00:00
|
|
|
|
|
|
|
|
|
this.reconnectTimer = setTimeout(() => {
|
2025-05-16 06:01:38 +00:00
|
|
|
|
if (!this.isWebSocketConnected) {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.connectWebSocket().catch((error) => {
|
|
|
|
|
console.error("自动重连失败:", error);
|
|
|
|
|
this.showRefreshButton = true;
|
|
|
|
|
});
|
2025-05-16 06:01:38 +00:00
|
|
|
|
}
|
|
|
|
|
}, this.reconnectInterval);
|
|
|
|
|
} else {
|
|
|
|
|
console.log("达到最大重连次数,停止重连");
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message.error(this.$t("chat.connectionFailed"));
|
2025-05-23 06:46:29 +00:00
|
|
|
|
this.isReconnecting = false;
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.showRefreshButton = true;
|
2025-05-23 06:46:29 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// 处理网络状态变化
|
|
|
|
|
handleNetworkChange() {
|
|
|
|
|
this.networkStatus = navigator.onLine ? "online" : "offline";
|
|
|
|
|
|
|
|
|
|
if (navigator.onLine) {
|
|
|
|
|
// 网络恢复时,尝试重连
|
|
|
|
|
if (!this.isWebSocketConnected) {
|
|
|
|
|
this.handleDisconnect();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 网络断开时,显示提示
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message.warning(this.$t("chat.CheckNetwork"));//连接失败,请检查网络后重试
|
2025-05-16 06:01:38 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 开始活动检测
|
|
|
|
|
startActivityCheck() {
|
|
|
|
|
this.activityCheckInterval = setInterval(() => {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const inactiveTime = now - this.lastActivityTime;
|
|
|
|
|
|
|
|
|
|
// 如果用户超过5分钟没有活动,且连接断开,则尝试重连
|
|
|
|
|
if (inactiveTime > 5 * 60 * 1000 && !this.isWebSocketConnected) {
|
|
|
|
|
this.handleDisconnect();
|
|
|
|
|
}
|
|
|
|
|
}, 60000); // 每分钟检查一次
|
|
|
|
|
},
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 更新最后活动时间
|
|
|
|
|
updateLastActivityTime() {
|
|
|
|
|
this.lastActivityTime = Date.now();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 页面关闭时的处理
|
2025-04-30 07:22:35 +00:00
|
|
|
|
handleBeforeUnload() {
|
|
|
|
|
this.disconnectWebSocket();
|
|
|
|
|
},
|
2025-05-30 08:39:09 +00:00
|
|
|
|
//只能输入300个字符
|
|
|
|
|
handleInputMessage() {
|
|
|
|
|
if (this.inputMessage.length > this.maxMessageLength) {
|
|
|
|
|
this.inputMessage = this.inputMessage.slice(0, this.maxMessageLength);
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 发送消息
|
|
|
|
|
sendMessage() {
|
2025-05-16 06:01:38 +00:00
|
|
|
|
if (!this.inputMessage.trim()) return;
|
2025-05-30 08:39:09 +00:00
|
|
|
|
if (this.inputMessage.length > this.maxMessageLength) {
|
|
|
|
|
this.$message.warning(`消息不能超过${this.maxMessageLength}个字符`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-05-16 06:01:38 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 检查 WebSocket 连接状态
|
|
|
|
|
if (!this.stompClient || !this.stompClient.connected) {
|
|
|
|
|
console.log("发送消息时连接已断开,尝试重连...");
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message.warning(this.$t("chat.attemptToReconnect"));//连接已断开,正在尝试重连...
|
2025-05-23 06:46:29 +00:00
|
|
|
|
this.handleDisconnect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
|
|
|
|
const messageText = this.inputMessage.trim();
|
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
try {
|
|
|
|
|
// 添加用户消息到界面
|
|
|
|
|
this.messages.push({
|
|
|
|
|
type: "user",
|
|
|
|
|
text: messageText,
|
2025-05-28 07:01:22 +00:00
|
|
|
|
time: new Date().toISOString(),
|
2025-05-16 06:01:38 +00:00
|
|
|
|
email: this.receivingEmail,
|
|
|
|
|
receiveUserType: 2, //接收消息用户类型
|
|
|
|
|
roomId: this.roomId,
|
|
|
|
|
isRead: false, // 新发送的消息默认未读
|
|
|
|
|
});
|
|
|
|
|
const message = {
|
|
|
|
|
content: messageText,
|
|
|
|
|
type: 1, // 1 表示文字消息
|
|
|
|
|
email: this.receivingEmail,
|
|
|
|
|
receiveUserType: 2,
|
|
|
|
|
roomId: this.roomId,
|
|
|
|
|
};
|
|
|
|
|
// 发送消息
|
|
|
|
|
this.stompClient.send(
|
|
|
|
|
"/point/send/message/to/customer",
|
|
|
|
|
{},
|
|
|
|
|
JSON.stringify(message)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 通过 WebSocket 发送消息
|
|
|
|
|
// if (this.stompClient && this.stompClient.connected) {
|
|
|
|
|
// this.stompClient.send({
|
|
|
|
|
// destination: "/point/send/message",
|
|
|
|
|
// body: JSON.stringify({
|
|
|
|
|
// content: messageText,
|
|
|
|
|
// type: 1,
|
|
|
|
|
// email: this.receivingEmail,
|
|
|
|
|
// receiveUserType:2,//当前用户类型
|
|
|
|
|
// roomId: this.roomId,
|
|
|
|
|
// }),
|
|
|
|
|
// });
|
|
|
|
|
// } else {
|
|
|
|
|
// this.handleAutoResponse(messageText);
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
this.inputMessage = "";
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("发送消息失败:", error);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message.error(this.$t("chat.sendFailed"));//发送消息失败,请重试
|
2025-05-16 06:01:38 +00:00
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 断开 WebSocket 连接
|
|
|
|
|
disconnectWebSocket() {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.stopHeartbeat();
|
2025-05-16 06:01:38 +00:00
|
|
|
|
if (this.stompClient) {
|
|
|
|
|
try {
|
|
|
|
|
// 取消所有订阅
|
|
|
|
|
if (this.stompClient.subscriptions) {
|
|
|
|
|
Object.keys(this.stompClient.subscriptions).forEach((id) => {
|
|
|
|
|
this.stompClient.unsubscribe(id);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 断开连接
|
|
|
|
|
this.stompClient.deactivate();
|
|
|
|
|
this.isWebSocketConnected = false;
|
|
|
|
|
this.connectionStatus = "disconnected";
|
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
this.isReconnecting = false;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("断开 WebSocket 连接失败:", error);
|
2025-04-30 07:22:35 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-16 06:01:38 +00:00
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 处理页面可见性变化
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 处理页面可见性变化
|
2025-04-25 06:09:32 +00:00
|
|
|
|
handleVisibilityChange() {
|
|
|
|
|
// 当页面变为可见且聊天窗口已打开时,标记消息为已读
|
|
|
|
|
if (!document.hidden && this.isChatOpen && this.roomId) {
|
|
|
|
|
this.markMessagesAsRead();
|
|
|
|
|
}
|
2025-05-23 06:46:29 +00:00
|
|
|
|
|
|
|
|
|
// 添加新的重连逻辑
|
|
|
|
|
if (!document.hidden) {
|
|
|
|
|
// 页面变为可见时,检查连接状态
|
|
|
|
|
if (!this.isWebSocketConnected) {
|
|
|
|
|
this.handleDisconnect();
|
|
|
|
|
}
|
|
|
|
|
// 更新最后活动时间
|
|
|
|
|
this.updateLastActivityTime();
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 修改标记消息为已读的方法
|
2025-04-25 06:09:32 +00:00
|
|
|
|
async markMessagesAsRead() {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
if (!this.roomId || !this.userEmail) {
|
|
|
|
|
console.log("缺少必要参数,跳过标记已读");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
try {
|
|
|
|
|
const data = {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
roomId: this.roomId,
|
|
|
|
|
userType: this.userType,
|
2025-05-23 06:46:29 +00:00
|
|
|
|
email: this.userEmail,
|
2025-04-25 06:09:32 +00:00
|
|
|
|
};
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 添加重试机制
|
|
|
|
|
let retryCount = 0;
|
|
|
|
|
const maxRetries = 3;
|
|
|
|
|
let success = false;
|
|
|
|
|
|
|
|
|
|
while (retryCount < maxRetries && !success) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await getReadMessage(data);
|
|
|
|
|
if (response && response.code === 200) {
|
|
|
|
|
console.log("消息已标记为已读");
|
|
|
|
|
// 清除未读消息计数
|
|
|
|
|
this.unreadMessages = 0;
|
|
|
|
|
// 更新所有用户消息的已读状态
|
|
|
|
|
this.messages.forEach((msg) => {
|
|
|
|
|
if (msg.type === "user") {
|
|
|
|
|
msg.isRead = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
success = true;
|
|
|
|
|
} else {
|
|
|
|
|
console.warn(
|
|
|
|
|
`标记消息已读失败 (尝试 ${retryCount + 1}/${maxRetries}):`,
|
|
|
|
|
response
|
|
|
|
|
);
|
|
|
|
|
retryCount++;
|
|
|
|
|
if (retryCount < maxRetries) {
|
|
|
|
|
// 等待一段时间后重试
|
|
|
|
|
await new Promise((resolve) =>
|
|
|
|
|
setTimeout(resolve, 1000 * retryCount)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(
|
|
|
|
|
`标记消息已读出错 (尝试 ${retryCount + 1}/${maxRetries}):`,
|
|
|
|
|
error
|
|
|
|
|
);
|
|
|
|
|
retryCount++;
|
|
|
|
|
if (retryCount < maxRetries) {
|
|
|
|
|
// 等待一段时间后重试
|
|
|
|
|
await new Promise((resolve) =>
|
|
|
|
|
setTimeout(resolve, 1000 * retryCount)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
if (!success) {
|
|
|
|
|
console.warn("标记消息已读失败,已达到最大重试次数");
|
|
|
|
|
// 即使标记已读失败,也更新本地状态
|
2025-04-25 06:09:32 +00:00
|
|
|
|
this.unreadMessages = 0;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
this.messages.forEach((msg) => {
|
|
|
|
|
if (msg.type === "user") {
|
|
|
|
|
msg.isRead = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
console.error("标记消息已读出错:", error);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 即使出错,也更新本地状态
|
|
|
|
|
this.unreadMessages = 0;
|
|
|
|
|
this.messages.forEach((msg) => {
|
|
|
|
|
if (msg.type === "user") {
|
|
|
|
|
msg.isRead = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 加载历史消息
|
|
|
|
|
async loadHistoryMessages() {
|
2025-05-23 06:46:29 +00:00
|
|
|
|
if (this.isLoadingHistory || !this.roomId) return;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
this.isLoadingHistory = true;
|
|
|
|
|
try {
|
|
|
|
|
const response = await getHistory7({
|
|
|
|
|
roomId: this.roomId,
|
|
|
|
|
userType: this.userType,
|
|
|
|
|
email: this.userEmail,
|
|
|
|
|
});
|
|
|
|
|
console.log("历史消息数据:", response);
|
|
|
|
|
|
|
|
|
|
if (response?.code === 200 && Array.isArray(response.data)) {
|
|
|
|
|
// 处理历史消息
|
|
|
|
|
const historyMessages = response.data.map((msg) => ({
|
|
|
|
|
type: msg.isSelf === 1 ? "user" : "system",
|
|
|
|
|
text: msg.content,
|
|
|
|
|
isImage: msg.type === 2,
|
|
|
|
|
imageUrl: msg.type === 2 ? msg.content : null,
|
|
|
|
|
time: new Date(msg.createTime),
|
|
|
|
|
id: msg.id,
|
|
|
|
|
roomId: msg.roomId,
|
|
|
|
|
sender: msg.sendEmail,
|
|
|
|
|
isHistory: true,
|
|
|
|
|
isRead: true,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 按时间顺序排序
|
|
|
|
|
this.messages = historyMessages.sort(
|
|
|
|
|
(a, b) => new Date(a.time) - new Date(b.time)
|
|
|
|
|
);
|
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 保持对话框打开状态
|
|
|
|
|
this.isChatOpen = true;
|
|
|
|
|
this.isMinimized = false;
|
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 等待 DOM 更新和图片加载完成后再滚动
|
|
|
|
|
await this.$nextTick();
|
|
|
|
|
// 添加一个小延时确保所有内容都渲染完成
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.scrollToBottom(true); // 传入 true 表示强制滚动
|
|
|
|
|
}, 100);
|
|
|
|
|
} else {
|
|
|
|
|
this.messages = [
|
|
|
|
|
{
|
|
|
|
|
type: "system",
|
2025-05-28 07:01:22 +00:00
|
|
|
|
text: this.$t("chat.noHistory")||"暂无历史消息",
|
2025-05-23 06:46:29 +00:00
|
|
|
|
isSystemHint: true,
|
|
|
|
|
time: new Date(),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("加载历史消息失败:", error);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message.error(this.$t("chat.historicalFailure"));//加载历史消息失败
|
2025-05-23 06:46:29 +00:00
|
|
|
|
this.messages = [
|
|
|
|
|
{
|
|
|
|
|
type: "system",
|
2025-05-28 07:01:22 +00:00
|
|
|
|
text: this.$t("chat.historicalFailure")||"加载历史消息失败,请重试",
|
2025-05-23 06:46:29 +00:00
|
|
|
|
isSystemHint: true,
|
2025-05-28 07:01:22 +00:00
|
|
|
|
time: new Date().toISOString(),
|
2025-05-23 06:46:29 +00:00
|
|
|
|
isError: true,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
} finally {
|
|
|
|
|
this.isLoadingHistory = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 加载更多历史消息(超过7天的)
|
|
|
|
|
async loadMoreHistory() {
|
2025-05-23 06:46:29 +00:00
|
|
|
|
if (this.isLoadingHistory || !this.roomId) return;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 保持对话框打开状态
|
|
|
|
|
this.isChatOpen = true;
|
|
|
|
|
this.isMinimized = false;
|
2025-05-23 06:46:29 +00:00
|
|
|
|
this.isLoadingHistory = true;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
try {
|
|
|
|
|
// 获取当前消息列表中最旧消息的 ID
|
|
|
|
|
const oldestMessage = this.messages.find(
|
|
|
|
|
(msg) => !msg.isSystemHint && !msg.isLoading
|
|
|
|
|
);
|
|
|
|
|
if (!oldestMessage || !oldestMessage.id) {
|
|
|
|
|
console.warn("没有找到有效的消息ID");
|
|
|
|
|
this.hasMoreHistory = false;
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 添加没有更多消息的提示
|
|
|
|
|
this.messages.unshift({
|
|
|
|
|
type: "system",
|
|
|
|
|
text: this.$t("chat.noMoreHistory")||"没有更多历史消息了",
|
|
|
|
|
isSystemHint: true,
|
|
|
|
|
time: new Date(),
|
|
|
|
|
});
|
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 loadingMsg = {
|
|
|
|
|
type: "system",
|
2025-05-28 07:01:22 +00:00
|
|
|
|
text: this.$t("chat.loadingHistory")||"正在加载更多历史消息...",
|
2025-05-23 06:46:29 +00:00
|
|
|
|
isLoading: true,
|
|
|
|
|
time: new Date(),
|
|
|
|
|
};
|
|
|
|
|
this.messages.unshift(loadingMsg);
|
|
|
|
|
|
|
|
|
|
// 获取更早的聊天记录,添加 id 参数
|
|
|
|
|
const response = await getHistory7({
|
|
|
|
|
roomId: this.roomId,
|
|
|
|
|
userType: this.userType,
|
|
|
|
|
email: this.userEmail,
|
|
|
|
|
id: oldestMessage.id, // 添加最旧消息的 ID
|
|
|
|
|
});
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 移除加载中提示
|
|
|
|
|
this.messages = this.messages.filter((msg) => !msg.isLoading);
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
if (
|
|
|
|
|
response &&
|
|
|
|
|
response.code === 200 &&
|
|
|
|
|
response.data &&
|
|
|
|
|
response.data.length > 0
|
|
|
|
|
) {
|
|
|
|
|
// 处理并添加历史消息
|
|
|
|
|
const historyMessages = this.formatHistoryMessages(response.data);
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 将历史消息添加到消息列表的前面
|
|
|
|
|
this.messages = [...historyMessages, ...this.messages];
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 如果没有数据返回,表示没有更多历史记录
|
|
|
|
|
this.hasMoreHistory = historyMessages.length > 0;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
if (historyMessages.length === 0) {
|
|
|
|
|
this.messages.unshift({
|
|
|
|
|
type: "system",
|
2025-05-28 07:01:22 +00:00
|
|
|
|
text: this.$t("chat.noMoreHistory")||"没有更多历史消息了",
|
2025-05-23 06:46:29 +00:00
|
|
|
|
isSystemHint: true,
|
|
|
|
|
time: new Date(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.hasMoreHistory = false;
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 添加没有更多消息的提示
|
2025-05-23 06:46:29 +00:00
|
|
|
|
this.messages.unshift({
|
|
|
|
|
type: "system",
|
|
|
|
|
text: "没有更多历史消息了",
|
|
|
|
|
isSystemHint: true,
|
|
|
|
|
time: new Date(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("加载更多历史消息失败:", error);
|
2025-04-25 06:09:32 +00:00
|
|
|
|
this.messages.unshift({
|
|
|
|
|
type: "system",
|
2025-05-28 07:01:22 +00:00
|
|
|
|
text: this.$t("chat.historicalFailure")||"加载更多历史消息失败",
|
2025-05-23 06:46:29 +00:00
|
|
|
|
isError: true,
|
2025-04-25 06:09:32 +00:00
|
|
|
|
time: new Date(),
|
|
|
|
|
});
|
2025-05-23 06:46:29 +00:00
|
|
|
|
} finally {
|
|
|
|
|
this.isLoadingHistory = false;
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 确保在加载完成后仍然保持对话框打开状态
|
|
|
|
|
this.isChatOpen = true;
|
|
|
|
|
this.isMinimized = false;
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
2025-05-23 06:46:29 +00:00
|
|
|
|
},
|
2025-04-22 06:26:41 +00:00
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 格式化历史消息数据
|
|
|
|
|
formatHistoryMessages(messagesData) {
|
2025-05-16 06:01:38 +00:00
|
|
|
|
if (!messagesData || !Array.isArray(messagesData)) return [];
|
|
|
|
|
|
|
|
|
|
return messagesData
|
|
|
|
|
.map((msg) => ({
|
|
|
|
|
type: msg.isSelf === 1 ? "user" : "system", // 根据 isSelf 判断消息类型
|
|
|
|
|
text: msg.content || "",
|
|
|
|
|
isImage: msg.type === 2,
|
2025-05-28 07:01:22 +00:00
|
|
|
|
imageUrl: msg.type === 2 ? msg.content : null, // 图片消息直接使用content作为imageUrl
|
2025-05-16 06:01:38 +00:00
|
|
|
|
time: new Date(msg.createTime),
|
|
|
|
|
id: msg.id,
|
|
|
|
|
roomId: msg.roomId,
|
|
|
|
|
sender: msg.sendEmail,
|
|
|
|
|
isHistory: true,
|
|
|
|
|
isRead: true,
|
|
|
|
|
}))
|
|
|
|
|
.sort((a, b) => new Date(a.time) - new Date(b.time));
|
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 修改 fetchUserid 方法,添加 token 检查
|
2025-05-16 06:01:38 +00:00
|
|
|
|
async fetchUserid(params) {
|
2025-04-25 06:09:32 +00:00
|
|
|
|
try {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 先检查是否有 token
|
2025-05-16 06:01:38 +00:00
|
|
|
|
// const token = localStorage.getItem("token");
|
|
|
|
|
// if (!token) {
|
|
|
|
|
// console.log("用户未登录,不发起 getUserid 请求");
|
|
|
|
|
// // 对于未登录用户,可以生成一个临时 ID
|
|
|
|
|
// this.roomId = `guest_${Date.now()}`;
|
|
|
|
|
// this.receivingEmail = "customer_service@example.com"; // 或默认客服邮箱
|
|
|
|
|
// return null;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
const res = await getUserid(params);
|
2025-04-25 06:09:32 +00:00
|
|
|
|
if (res && res.code == 200) {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
console.log("获取用户ID成功:", res);
|
2025-04-25 06:09:32 +00:00
|
|
|
|
this.receivingEmail = res.data.userEmail;
|
|
|
|
|
this.roomId = res.data.id;
|
|
|
|
|
return res.data;
|
|
|
|
|
} else {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
console.warn("获取用户ID未返回有效数据");
|
2025-04-25 06:09:32 +00:00
|
|
|
|
return null;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
} catch (error) {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
console.error("获取用户ID失败:", error);
|
2025-04-25 06:09:32 +00:00
|
|
|
|
throw error;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
|
|
|
|
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 添加新方法:更新消息已读状态
|
|
|
|
|
updateMessageReadStatus(messageIds) {
|
|
|
|
|
if (!Array.isArray(messageIds) || messageIds.length === 0) {
|
|
|
|
|
// 如果没有具体的消息ID,就更新所有用户消息为已读
|
|
|
|
|
this.messages.forEach((msg) => {
|
|
|
|
|
if (msg.type === "user") {
|
|
|
|
|
msg.isRead = true;
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-30 07:22:35 +00:00
|
|
|
|
} else {
|
|
|
|
|
// 更新指定ID的消息为已读
|
|
|
|
|
this.messages.forEach((msg) => {
|
|
|
|
|
if (msg.id && messageIds.includes(msg.id)) {
|
|
|
|
|
msg.isRead = true;
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
});
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 接收消息处理
|
|
|
|
|
onMessageReceived(message) {
|
2025-05-16 06:01:38 +00:00
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(message.body);
|
|
|
|
|
console.log("收到新消息:", data);
|
|
|
|
|
|
|
|
|
|
// 构造消息对象
|
|
|
|
|
const messageObj = {
|
2025-05-23 06:46:29 +00:00
|
|
|
|
type: data.sendEmail === this.userEmail ? "user" : "system",
|
2025-05-16 06:01:38 +00:00
|
|
|
|
text: data.content,
|
|
|
|
|
isImage: data.type === 2,
|
2025-05-28 07:01:22 +00:00
|
|
|
|
imageUrl: data.type === 2 ? data.content : null, // 图片消息直接使用content作为imageUrl
|
|
|
|
|
time: new Date(data.sendTime).toISOString(),
|
2025-05-16 06:01:38 +00:00
|
|
|
|
id: data.id,
|
|
|
|
|
roomId: data.roomId,
|
|
|
|
|
sender: data.sendEmail,
|
|
|
|
|
isRead: false,
|
|
|
|
|
};
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
// 直接添加到消息列表
|
|
|
|
|
this.messages.push(messageObj);
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
// 如果聊天窗口未打开,增加未读消息数
|
|
|
|
|
if (!this.isChatOpen) {
|
|
|
|
|
// 使用服务器返回的未读数,如果没有则增加1
|
|
|
|
|
if (data.clientReadNum !== undefined) {
|
|
|
|
|
this.unreadMessages = data.clientReadNum;
|
|
|
|
|
} else {
|
|
|
|
|
this.unreadMessages++;
|
|
|
|
|
}
|
|
|
|
|
// 显示消息通知
|
|
|
|
|
this.showNotification(messageObj);
|
|
|
|
|
} else {
|
|
|
|
|
// 如果聊天窗口已打开,立即标记为已读
|
|
|
|
|
this.markMessagesAsRead();
|
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
// 滚动到底部
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("处理消息失败:", error);
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 显示消息通知
|
|
|
|
|
showNotification(message) {
|
|
|
|
|
if (!("Notification" in window)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查通知权限
|
|
|
|
|
if (Notification.permission === "granted") {
|
|
|
|
|
this.createNotification(message);
|
|
|
|
|
} else if (Notification.permission !== "denied") {
|
|
|
|
|
// 请求权限
|
|
|
|
|
Notification.requestPermission().then((permission) => {
|
|
|
|
|
if (permission === "granted") {
|
|
|
|
|
this.createNotification(message);
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
});
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 创建通知
|
|
|
|
|
createNotification(message) {
|
|
|
|
|
const notification = new Notification("新消息", {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
body: message.isImage ? `[ ${this.$t("chat.pictureMessage")}]`|| "[图片消息]" : message.text,
|
2025-04-30 07:22:35 +00:00
|
|
|
|
icon: "/path/to/notification-icon.png", // 添加适当的图标
|
|
|
|
|
});
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
2025-04-30 07:22:35 +00:00
|
|
|
|
notification.onclick = () => {
|
|
|
|
|
// 点击通知时打开聊天窗口
|
|
|
|
|
window.focus();
|
|
|
|
|
this.openChat(message.roomId);
|
|
|
|
|
};
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
|
|
|
|
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 打开聊天窗口
|
|
|
|
|
async openChat(roomId) {
|
|
|
|
|
this.isChatOpen = true;
|
|
|
|
|
this.isMinimized = false;
|
|
|
|
|
|
|
|
|
|
if (roomId) {
|
|
|
|
|
this.currentContactId = roomId;
|
|
|
|
|
this.messages = this.cachedMessages[roomId] || [];
|
|
|
|
|
this.markMessagesAsRead(roomId);
|
|
|
|
|
|
|
|
|
|
// 等待 DOM 更新后滚动到底部
|
|
|
|
|
await this.$nextTick();
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
|
|
|
|
|
2025-05-16 06:01:38 +00:00
|
|
|
|
// 打开聊天框
|
2025-04-30 07:22:35 +00:00
|
|
|
|
async toggleChat() {
|
2025-05-23 06:46:29 +00:00
|
|
|
|
this.isChatOpen = !this.isChatOpen;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 1. 判别身份
|
2025-05-30 08:39:09 +00:00
|
|
|
|
const userInfo = JSON.parse(
|
|
|
|
|
localStorage.getItem("jurisdiction") || "{}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (userInfo.roleKey === "customer_service") {
|
|
|
|
|
// 客服用户 跳转到客服页面
|
|
|
|
|
this.userType = 2;
|
|
|
|
|
const lang = this.$i18n.locale;
|
|
|
|
|
this.$router.push(`/${lang}/customerService`);
|
|
|
|
|
return;
|
|
|
|
|
// this.userEmail = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
if (this.isChatOpen) {
|
|
|
|
|
try {
|
|
|
|
|
// 确定用户类型
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// this.determineUserType();
|
2025-05-23 06:46:29 +00:00
|
|
|
|
|
|
|
|
|
// 如果未连接或连接断开,则重新初始化 WebSocket
|
|
|
|
|
if (
|
|
|
|
|
!this.isWebSocketConnected ||
|
|
|
|
|
this.connectionStatus === "disconnected"
|
|
|
|
|
) {
|
|
|
|
|
await this.connectWebSocket();
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
2025-05-23 06:46:29 +00:00
|
|
|
|
// 如果消息列表为空,加载历史消息
|
|
|
|
|
if (this.messages.length === 0) {
|
|
|
|
|
await this.loadHistoryMessages();
|
|
|
|
|
} else {
|
|
|
|
|
// 如果已有消息,确保滚动到底部
|
|
|
|
|
await this.$nextTick();
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.scrollToBottom(true);
|
|
|
|
|
}, 100);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 标记消息为已读,但不等待其完成
|
|
|
|
|
this.markMessagesAsRead().catch((error) => {
|
|
|
|
|
console.warn("标记消息已读失败,但不影响用户体验:", error);
|
|
|
|
|
});
|
2025-05-23 06:46:29 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("初始化聊天失败:", error);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message.error(this.$t("chat.initializationFailed")||"初始化聊天失败,请重试");
|
2025-05-23 06:46:29 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
2025-04-30 07:22:35 +00:00
|
|
|
|
minimizeChat() {
|
|
|
|
|
this.isChatOpen = false;
|
|
|
|
|
this.isMinimized = true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
closeChat() {
|
|
|
|
|
this.isChatOpen = false;
|
|
|
|
|
this.isMinimized = true;
|
|
|
|
|
// this.disconnectWebSocket(); // 关闭 WebSocket 连接
|
|
|
|
|
},
|
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 添加系统消息
|
|
|
|
|
addSystemMessage(text) {
|
|
|
|
|
this.messages.push({
|
|
|
|
|
type: "system",
|
|
|
|
|
text: text,
|
|
|
|
|
isImage: false,
|
|
|
|
|
time: new Date(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 滚动到底部
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 自动回复 (仅在无法连接服务器时使用)
|
|
|
|
|
handleAutoResponse(message) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
let response =
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$t("chat.beSorry")||"抱歉,我暂时无法回答这个问题。请排队等待人工客服或提交工单。";
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
// 检查是否匹配自动回复关键词
|
|
|
|
|
for (const [keyword, reply] of Object.entries(this.autoResponses)) {
|
|
|
|
|
if (message.toLowerCase().includes(keyword.toLowerCase())) {
|
|
|
|
|
response = reply;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加系统回复
|
|
|
|
|
this.messages.push({
|
|
|
|
|
type: "system",
|
|
|
|
|
text: response,
|
|
|
|
|
isImage: false,
|
|
|
|
|
time: new Date(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!this.isChatOpen) {
|
|
|
|
|
this.unreadMessages++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
});
|
|
|
|
|
}, 1000);
|
|
|
|
|
},
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 滚动到消息列表顶部检测,用于加载更多历史消息
|
|
|
|
|
handleChatScroll() {
|
|
|
|
|
if (!this.$refs.chatBody) return;
|
|
|
|
|
|
|
|
|
|
const { scrollTop } = this.$refs.chatBody;
|
|
|
|
|
// 当滚动到顶部时,加载更多历史消息
|
|
|
|
|
if (scrollTop < 50 && this.hasMoreHistory && !this.isLoadingHistory) {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 保持对话框打开状态
|
|
|
|
|
this.isChatOpen = true;
|
|
|
|
|
this.isMinimized = false;
|
2025-04-25 06:09:32 +00:00
|
|
|
|
this.loadMoreHistory();
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
2025-05-28 07:01:22 +00:00
|
|
|
|
|
|
|
|
|
// <!-- 新增加载完成事件 -->
|
|
|
|
|
handleImageLoad() {
|
|
|
|
|
this.scrollToBottom(true); // 强制立即滚动
|
|
|
|
|
},
|
2025-05-16 06:01:38 +00:00
|
|
|
|
//滚动到底部
|
|
|
|
|
scrollToBottom(force = false) {
|
2025-05-23 06:46:29 +00:00
|
|
|
|
if (!this.$refs.chatBody) return;
|
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
const scrollOptions = {
|
|
|
|
|
top: this.$refs.chatBody.scrollHeight,
|
|
|
|
|
behavior: force ? "auto" : "smooth",
|
|
|
|
|
};
|
2025-05-23 06:46:29 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
try {
|
|
|
|
|
// 尝试使用平滑滚动
|
|
|
|
|
this.$refs.chatBody.scrollTo(scrollOptions);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// 兼容性处理
|
|
|
|
|
this.$refs.chatBody.scrollTop = this.$refs.chatBody.scrollHeight;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// const scrollOptions = {
|
|
|
|
|
// top: this.$refs.chatBody.scrollHeight,
|
|
|
|
|
// behavior: force ? "auto" : "smooth", // 强制滚动时使用 'auto'
|
|
|
|
|
// };
|
|
|
|
|
|
|
|
|
|
// try {
|
|
|
|
|
// this.$refs.chatBody.scrollTo(scrollOptions);
|
|
|
|
|
// } catch (error) {
|
|
|
|
|
// // 如果平滑滚动不支持,则直接设置
|
|
|
|
|
// this.$refs.chatBody.scrollTop = this.$refs.chatBody.scrollHeight;
|
|
|
|
|
// }
|
2025-05-23 06:46:29 +00:00
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
formatTime(date) {
|
2025-04-30 07:22:35 +00:00
|
|
|
|
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
|
|
|
|
|
return ""; // 处理无效日期
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
|
const yesterday = new Date(today);
|
|
|
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
|
|
|
|
|
|
// 消息日期(只保留日期部分,不含时间)
|
|
|
|
|
const messageDate = new Date(
|
|
|
|
|
date.getFullYear(),
|
|
|
|
|
date.getMonth(),
|
|
|
|
|
date.getDate()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 格式化时间部分 (HH:MM)
|
|
|
|
|
const timeString = date.toLocaleTimeString([], {
|
2025-04-25 06:09:32 +00:00
|
|
|
|
hour: "2-digit",
|
|
|
|
|
minute: "2-digit",
|
|
|
|
|
});
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
|
|
|
|
// 判断消息是今天、昨天还是更早的日期
|
|
|
|
|
if (messageDate.getTime() === today.getTime()) {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
return `${this.$t("chat.today")} ${timeString}`;//今天
|
2025-04-30 07:22:35 +00:00
|
|
|
|
} else if (messageDate.getTime() === yesterday.getTime()) {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
return `${this.$t("chat.yesterday")} ${timeString}`;//昨天
|
2025-04-30 07:22:35 +00:00
|
|
|
|
} else {
|
|
|
|
|
// 超过两天的消息显示完整日期
|
|
|
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
|
|
|
|
|
2,
|
|
|
|
|
"0"
|
|
|
|
|
)}-${String(date.getDate()).padStart(2, "0")} ${timeString}`;
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleClickOutside(event) {
|
|
|
|
|
if (this.isChatOpen) {
|
|
|
|
|
const chatElement = this.$el.querySelector(".chat-dialog");
|
|
|
|
|
const chatIcon = this.$el.querySelector(".chat-icon");
|
2025-05-28 07:01:22 +00:00
|
|
|
|
const historyIndicator = this.$el.querySelector(".history-indicator");
|
|
|
|
|
|
|
|
|
|
// 如果点击的是历史消息加载指示器,不关闭对话框
|
|
|
|
|
if (historyIndicator && historyIndicator.contains(event.target)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
chatElement &&
|
|
|
|
|
!chatElement.contains(event.target) &&
|
|
|
|
|
!chatIcon.contains(event.target)
|
|
|
|
|
) {
|
|
|
|
|
this.isChatOpen = false;
|
|
|
|
|
}
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 处理图片上传
|
2025-04-30 07:22:35 +00:00
|
|
|
|
async handleImageUpload(event) {
|
2025-05-16 06:01:38 +00:00
|
|
|
|
if (this.connectionStatus !== "connected") {
|
|
|
|
|
console.log("当前连接状态:", this.connectionStatus);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
const file = event.target.files[0];
|
|
|
|
|
if (!file) return;
|
|
|
|
|
if (!file.type.startsWith("image/")) {
|
|
|
|
|
this.$message({
|
|
|
|
|
message: this.$t("chat.onlyImages") || "只能上传图片文件!",
|
|
|
|
|
type: "warning",
|
|
|
|
|
});
|
|
|
|
|
return;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
const maxSize = 5 * 1024 * 1024;
|
|
|
|
|
if (file.size > maxSize) {
|
|
|
|
|
this.$message({
|
|
|
|
|
message: this.$t("chat.imageTooLarge") || "图片大小不能超过5MB!",
|
|
|
|
|
type: "warning",
|
|
|
|
|
});
|
|
|
|
|
return;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
try {
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message({ message: this.$t("chat.uploading")||"正在上传图片...", type: "info" });
|
|
|
|
|
// 创建 FormData
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append("file", file);
|
|
|
|
|
|
|
|
|
|
// 上传图片
|
|
|
|
|
const response = await this.$axios({
|
|
|
|
|
method: "post",
|
|
|
|
|
url: `${process.env.VUE_APP_BASE_API}pool/ticket/uploadFile`,
|
|
|
|
|
data: formData,
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "multipart/form-data",
|
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
});
|
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
if (response.data.code === 200) {
|
|
|
|
|
const imageUrl = response.data.data.url;
|
|
|
|
|
// 本地立即显示图片(使用base64预览)
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
const base64Image = e.target.result;
|
|
|
|
|
this.messages.push({
|
|
|
|
|
type: "user",
|
|
|
|
|
text: "",
|
|
|
|
|
isImage: true,
|
|
|
|
|
imageUrl: base64Image, // 本地预览用base64
|
|
|
|
|
time: new Date().toISOString(),
|
2025-04-25 06:09:32 +00:00
|
|
|
|
email: this.receivingEmail,
|
2025-05-28 07:01:22 +00:00
|
|
|
|
sendUserType: this.userType,
|
2025-04-30 07:22:35 +00:00
|
|
|
|
roomId: this.roomId,
|
2025-05-28 07:01:22 +00:00
|
|
|
|
isRead: false,
|
|
|
|
|
});
|
|
|
|
|
// 发送图片消息
|
|
|
|
|
this.sendImageMessage(imageUrl);
|
2025-05-16 06:01:38 +00:00
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.scrollToBottom();
|
|
|
|
|
});
|
2025-05-28 07:01:22 +00:00
|
|
|
|
};
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(response.data.msg || this.$t("chat.pictureFailed")||"发送图片失败,请重试");
|
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
} catch (error) {
|
2025-05-16 06:01:38 +00:00
|
|
|
|
console.error("图片处理失败:", error);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
this.$message.error(this.$t("chat.processingFailed")||"图片处理失败,请重试");
|
2025-04-30 07:22:35 +00:00
|
|
|
|
} finally {
|
|
|
|
|
this.$refs.imageUpload.value = "";
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
2025-05-28 07:01:22 +00:00
|
|
|
|
// 发送图片消息
|
|
|
|
|
sendImageMessage(imageUrl) {
|
|
|
|
|
if (!this.stompClient || !this.stompClient.connected) {
|
|
|
|
|
console.log("发送消息时连接已断开,尝试重连...");
|
|
|
|
|
this.$message.warning(this.$t("chat.attemptToReconnect")||"连接已断开,正在尝试重连...");
|
|
|
|
|
this.handleDisconnect();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const message = {
|
|
|
|
|
type: 2, // 2 表示图片消息
|
|
|
|
|
email: this.receivingEmail,
|
|
|
|
|
receiveUserType: 2,
|
|
|
|
|
roomId: this.roomId,
|
|
|
|
|
content: imageUrl, // 使用接口返回的url
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.stompClient.send(
|
|
|
|
|
"/point/send/message/to/customer",
|
|
|
|
|
{},
|
|
|
|
|
JSON.stringify(message)
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("发送图片消息失败:", error);
|
|
|
|
|
this.$message.error(this.$t("chat.pictureFailed")||"发送图片失败,请重试");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 预览图片
|
|
|
|
|
previewImage(imageUrl) {
|
|
|
|
|
this.previewImageUrl = imageUrl;
|
|
|
|
|
this.showImagePreview = true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 关闭图片预览
|
|
|
|
|
closeImagePreview() {
|
|
|
|
|
this.showImagePreview = false;
|
|
|
|
|
this.previewImageUrl = "";
|
|
|
|
|
},
|
2025-05-28 07:01:22 +00:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 新增:重试连接按钮处理
|
|
|
|
|
*/
|
|
|
|
|
async handleRetryConnect() {
|
|
|
|
|
try {
|
|
|
|
|
// 重置所有状态
|
|
|
|
|
this.connectionStatus = "connecting";
|
|
|
|
|
this.isReconnecting = true;
|
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
this.connectionError = null;
|
|
|
|
|
this.showRefreshButton = false;
|
|
|
|
|
|
|
|
|
|
// 如果存在旧的连接,先断开
|
|
|
|
|
if (this.stompClient) {
|
|
|
|
|
try {
|
|
|
|
|
this.stompClient.disconnect();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn("断开旧连接时出错:", error);
|
|
|
|
|
}
|
|
|
|
|
this.stompClient = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重新初始化用户身份
|
|
|
|
|
await this.determineUserType();
|
|
|
|
|
|
|
|
|
|
// 如果没有 userEmail,重新初始化身份
|
|
|
|
|
if (!this.userEmail) {
|
|
|
|
|
console.log("重新初始化用户身份...");
|
|
|
|
|
await this.determineUserType();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 重新连接 WebSocket
|
|
|
|
|
console.log("开始重新连接...");
|
|
|
|
|
await this.connectWebSocket(this.userEmail);
|
|
|
|
|
|
|
|
|
|
// 如果连接成功,重新加载历史消息
|
|
|
|
|
if (this.isWebSocketConnected) {
|
|
|
|
|
console.log("连接成功,重新加载历史消息...");
|
|
|
|
|
await this.loadHistoryMessages();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("重试连接失败:", error);
|
|
|
|
|
this.connectionStatus = "error";
|
|
|
|
|
this.isReconnecting = false;
|
|
|
|
|
this.connectionError = error.message || this.$t("chat.retryFailed")||"重试连接失败,请刷新页面";
|
|
|
|
|
this.showRefreshButton = true;
|
|
|
|
|
this.$message.error(this.$t("chat.retryFailed")||"重试连接失败,请刷新页面重试");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 添加刷新页面方法
|
|
|
|
|
refreshPage() {
|
|
|
|
|
window.location.reload();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 添加心跳检测方法
|
|
|
|
|
startHeartbeat() {
|
|
|
|
|
this.stopHeartbeat();
|
|
|
|
|
|
|
|
|
|
this.lastHeartbeatTime = Date.now();
|
|
|
|
|
this.heartbeatInterval = setInterval(() => {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
if (now - this.lastHeartbeatTime > this.heartbeatTimeout) {
|
|
|
|
|
console.log("心跳超时,检查连接状态...");
|
|
|
|
|
// 只有在确实连接状态为已连接时才重连
|
|
|
|
|
if (
|
|
|
|
|
this.connectionStatus === "connected" &&
|
|
|
|
|
this.stompClient &&
|
|
|
|
|
this.stompClient.connected
|
|
|
|
|
) {
|
|
|
|
|
// 再次确认连接状态
|
|
|
|
|
if (this.stompClient.ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
console.log("WebSocket 连接仍然活跃,更新心跳时间");
|
|
|
|
|
this.lastHeartbeatTime = now;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
console.log("连接状态异常,准备重连...");
|
|
|
|
|
this.handleDisconnect();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, this.heartbeatCheckInterval);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
stopHeartbeat() {
|
|
|
|
|
if (this.heartbeatInterval) {
|
|
|
|
|
clearInterval(this.heartbeatInterval);
|
|
|
|
|
this.heartbeatInterval = null;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
handleLoginClick() {
|
|
|
|
|
this.isChatOpen = false;
|
|
|
|
|
const currentLang = this.$i18n.locale;
|
|
|
|
|
this.$router.push(`/${currentLang}/login`);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 统一处理连接数上限错误(支持多语言和后端错误码)
|
|
|
|
|
* @param {Object} errorObj - 推荐结构:{ code, error, message }
|
|
|
|
|
* @returns {boolean} 是否已处理(true=已处理,不再重连)
|
|
|
|
|
*/
|
|
|
|
|
handleConnectionError(errorObj) {
|
|
|
|
|
if (!errorObj) return false;
|
|
|
|
|
// 1. 优先判断 error/code 字段
|
|
|
|
|
if (errorObj.error === 'MAX_CONNECTIONS' || errorObj.code === 429) {
|
|
|
|
|
this.connectionError = this.$t('chat.maxConnectionsError') || '连接数已达上限,请刷新页面重试';
|
|
|
|
|
this.isReconnecting = false;
|
|
|
|
|
this.showRefreshButton = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// 2. 兜底判断 message 关键词(兼容历史/异常情况)
|
|
|
|
|
const msg = errorObj.message || '';
|
|
|
|
|
if (msg.includes('连接数已达上限') || msg.toLowerCase().includes('maximum connections')) {
|
|
|
|
|
this.connectionError = this.$t('chat.maxConnectionsError') || '连接数已达上限,请刷新页面重试';
|
|
|
|
|
this.isReconnecting = false;
|
|
|
|
|
this.showRefreshButton = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
},
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
// 移除滚动监听
|
|
|
|
|
if (this.$refs.chatBody) {
|
|
|
|
|
this.$refs.chatBody.removeEventListener("scroll", this.handleChatScroll);
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
// 移除页面可见性变化监听
|
2025-04-30 07:22:35 +00:00
|
|
|
|
document.removeEventListener(
|
|
|
|
|
"visibilitychange",
|
|
|
|
|
this.handleVisibilityChange
|
|
|
|
|
);
|
2025-05-16 06:01:38 +00:00
|
|
|
|
// 确保在销毁时断开连接
|
2025-05-23 06:46:29 +00:00
|
|
|
|
if (this.stompClient) {
|
|
|
|
|
this.stompClient.disconnect();
|
|
|
|
|
this.stompClient = null;
|
|
|
|
|
}
|
2025-04-30 07:22:35 +00:00
|
|
|
|
// 断开 WebSocket 连接
|
|
|
|
|
this.disconnectWebSocket();
|
2025-05-23 06:46:29 +00:00
|
|
|
|
|
|
|
|
|
// 清除活动检测定时器
|
|
|
|
|
if (this.activityCheckInterval) {
|
|
|
|
|
clearInterval(this.activityCheckInterval);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除重连定时器
|
|
|
|
|
if (this.reconnectTimer) {
|
|
|
|
|
clearTimeout(this.reconnectTimer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移除新添加的事件监听
|
|
|
|
|
window.removeEventListener("online", this.handleNetworkChange);
|
|
|
|
|
window.removeEventListener("offline", this.handleNetworkChange);
|
|
|
|
|
document.removeEventListener("mousemove", this.updateLastActivityTime);
|
|
|
|
|
document.removeEventListener("keydown", this.updateLastActivityTime);
|
|
|
|
|
document.removeEventListener("click", this.updateLastActivityTime);
|
2025-05-28 07:01:22 +00:00
|
|
|
|
|
|
|
|
|
this.stopHeartbeat();
|
2025-04-25 06:09:32 +00:00
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
</script>
|
2025-04-22 06:26:41 +00:00
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.chat-widget {
|
|
|
|
|
position: fixed;
|
|
|
|
|
bottom: 40px;
|
|
|
|
|
right: 60px;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-icon {
|
|
|
|
|
width: 60px;
|
|
|
|
|
height: 60px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background-color: #ac85e0;
|
|
|
|
|
color: white;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
font-size: 28px;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
background-color: #6e3edb;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
|
background-color: #6e3edb;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.unread-badge {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -5px;
|
|
|
|
|
right: -5px;
|
|
|
|
|
background-color: #e74c3c;
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-dialog {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 80px;
|
|
|
|
|
right: 0;
|
|
|
|
|
width: 350px;
|
|
|
|
|
height: 450px;
|
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-header {
|
|
|
|
|
background-color: #ac85e0;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 15px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-title {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 15px;
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
opacity: 0.8;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-body {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 15px;
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-status {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
margin-bottom: 16px;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
color: #666;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
&.connecting i {
|
|
|
|
|
color: #ac85e0;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
&.error {
|
2025-04-22 06:26:41 +00:00
|
|
|
|
i {
|
2025-04-25 06:09:32 +00:00
|
|
|
|
color: #e74c3c;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
color: #e74c3c;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
.retry-button {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: #ac85e0;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
cursor: pointer;
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: #6e3edb;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-empty {
|
|
|
|
|
color: #777;
|
|
|
|
|
text-align: center;
|
|
|
|
|
margin-top: 30px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-message {
|
|
|
|
|
display: flex;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
|
|
|
|
|
&.chat-message-user {
|
|
|
|
|
flex-direction: row-reverse;
|
|
|
|
|
|
|
|
|
|
.message-content {
|
|
|
|
|
background-color: #ac85e0;
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 18px 18px 0 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-time {
|
|
|
|
|
text-align: right;
|
|
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
}
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
|
|
|
|
|
&.chat-message-system {
|
|
|
|
|
.message-content {
|
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 18px 18px 18px 0;
|
|
|
|
|
}
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-avatar {
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
margin: 0 10px;
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
color: #555;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-content {
|
|
|
|
|
max-width: 70%;
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-text {
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-image {
|
|
|
|
|
img {
|
|
|
|
|
max-width: 200px;
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
border-radius: 8px;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
cursor: pointer;
|
2025-04-25 06:09:32 +00:00
|
|
|
|
transition: transform 0.2s;
|
|
|
|
|
|
2025-04-22 06:26:41 +00:00
|
|
|
|
&:hover {
|
2025-04-25 06:09:32 +00:00
|
|
|
|
transform: scale(1.03);
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-time {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: #999;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-footer {
|
|
|
|
|
padding: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
border-top: 1px solid #e0e0e0;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-toolbar {
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.image-upload-label {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 30px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
color: #666;
|
|
|
|
|
|
|
|
|
|
&:hover:not(.disabled) {
|
|
|
|
|
color: #ac85e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-input {
|
|
|
|
|
flex: 1;
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
padding: 8px 15px;
|
|
|
|
|
outline: none;
|
|
|
|
|
|
|
|
|
|
&:focus:not(:disabled) {
|
|
|
|
|
border-color: #ac85e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:disabled {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-send {
|
|
|
|
|
background-color: #ac85e0;
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
padding: 8px 15px;
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
|
|
background-color: #6e3edb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:disabled {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 图片预览
|
|
|
|
|
.image-preview-overlay {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
|
|
|
z-index: 1100;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.image-preview-container {
|
|
|
|
|
position: relative;
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
max-height: 90%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-image {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
max-height: 90vh;
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-close {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -40px;
|
|
|
|
|
right: 0;
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 动画效果
|
|
|
|
|
.chat-slide-enter-active,
|
|
|
|
|
.chat-slide-leave-active {
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-slide-enter,
|
|
|
|
|
.chat-slide-leave-to {
|
|
|
|
|
transform: translateY(20px);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移动端适配
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.chat-widget {
|
|
|
|
|
bottom: 20px;
|
|
|
|
|
right: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-dialog {
|
|
|
|
|
width: 300px;
|
|
|
|
|
height: 400px;
|
|
|
|
|
bottom: 70px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-image img {
|
|
|
|
|
max-width: 150px;
|
|
|
|
|
max-height: 150px;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.system-hint {
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #999;
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.history-indicator {
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #666;
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
padding: 5px;
|
|
|
|
|
border-radius: 15px;
|
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
width: fit-content;
|
|
|
|
|
margin: 0 auto 10px;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
&:hover {
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
color: #333;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-message-history {
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-message-loading,
|
|
|
|
|
.chat-message-hint {
|
|
|
|
|
margin: 5px 0;
|
|
|
|
|
justify-content: center;
|
2025-04-30 07:22:35 +00:00
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
span {
|
|
|
|
|
color: #999;
|
|
|
|
|
font-size: 12px;
|
2025-04-22 06:26:41 +00:00
|
|
|
|
}
|
2025-04-25 06:09:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-footer {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-time {
|
|
|
|
|
color: #999;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.message-read-status {
|
|
|
|
|
color: #999;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
margin-left: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chat-message-user .message-read-status {
|
|
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
|
|
}
|
2025-05-23 06:46:29 +00:00
|
|
|
|
|
|
|
|
|
.network-status {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 20px;
|
|
|
|
|
right: 20px;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
|
|
background-color: #fef0f0;
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
}
|
2025-05-28 07:01:22 +00:00
|
|
|
|
|
|
|
|
|
.guest-notice {
|
|
|
|
|
background-color: #f8f9fa;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
|
|
|
|
|
.guest-notice-content {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
color: #ac85e0;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.login-link {
|
|
|
|
|
color: #ac85e0;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color 0.3s;
|
|
|
|
|
margin: 0 2px;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #6e3edb;
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.error-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
.retry-button,
|
|
|
|
|
.refresh-button {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.retry-button {
|
|
|
|
|
background-color: #ac85e0;
|
|
|
|
|
color: white;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: #6e3edb;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.refresh-button {
|
|
|
|
|
background-color: #f0f0f0;
|
|
|
|
|
color: #666;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
background-color: #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-30 08:39:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.message-content {
|
|
|
|
|
position: relative;
|
|
|
|
|
max-width: 70%;
|
|
|
|
|
padding: 18px 15px 10px 15px; // 上方多留空间给时间
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
|
|
|
|
.message-time {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 6px;
|
|
|
|
|
right: 15px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: #bbb;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
user-select: none;
|
|
|
|
|
}
|
|
|
|
|
// 用户消息气泡内时间颜色适配
|
|
|
|
|
.chat-message-user & .message-time {
|
|
|
|
|
color: rgba(255,255,255,0.7);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-04-25 06:09:32 +00:00
|
|
|
|
</style>
|