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

1845 lines
50 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="cs-chat-container">
<!-- 聊天窗口主体 -->
<div class="cs-chat-wrapper">
<!-- 左侧联系人列表 -->
<div class="cs-contact-list">
<div class="cs-header"><i class="el-icon-s-custom"></i> 联系列表</div>
<div class="cs-search">
<el-input
prefix-icon="el-icon-search"
v-model="searchText"
placeholder="搜索最近联系人"
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)"
>
<div class="cs-avatar">
<i class="iconfont icon-icon28" style="font-size: 2vw"></i>
<span v-if="contact.unread" class="unread-badge">{{
contact.unread
}}</span>
<!-- 添加游客标识 -->
<span v-if="contact.isGuest" class="guest-badge">游客</span>
</div>
<div class="cs-contact-info">
<div class="cs-contact-name">
{{ contact.name }}
<span class="cs-contact-time">{{
formatLastTime(contact.lastTime)
}}</span>
</div>
<div class="cs-contact-msg">
<span v-if="contact.important" class="important-tag"
>[重要]</span
>
{{ contact.lastMessage }}
</div>
</div>
<!-- 添加重要标记图标 -->
<div
class="important-star"
:class="{ 'is-important': contact.important }"
@click.stop="toggleImportant(contact.roomId, !contact.important)"
title="标记为重要聊天"
>
<i class="el-icon-star-on"></i>
</div>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="cs-chat-area">
<!-- 顶部信息栏 -->
<div class="cs-chat-header">
<div class="cs-chat-title">
{{ currentContact ? currentContact.name : "请选择联系人" }}
<el-tag
v-if="currentContact && currentContact.important"
size="small"
type="danger"
@click="
toggleImportant(
currentContact.roomId,
!currentContact.important
)
"
>
重要
</el-tag>
<el-tag
v-else-if="currentContact"
size="small"
type="info"
@click="
toggleImportant(
currentContact.roomId,
!currentContact.important
)
"
>
标记为重要
</el-tag>
</div>
<div class="cs-header-actions">
<i class="el-icon-time" title="历史记录" @click="loadHistory"></i>
<!-- <i
class="el-icon-refresh"
title="刷新"
@click="refreshMessages"
></i> -->
<!-- <i class="el-icon-more" title="更多选项"></i> -->
</div>
</div>
<!-- 聊天内容区域 -->
<div class="cs-chat-messages" ref="messageContainer">
<div v-if="!currentContact" class="cs-empty-chat">
<i class="el-icon-chat-dot-round"></i>
<p>您尚未选择联系人</p>
</div>
<template v-else>
<!-- 加载更多历史消息按钮 -->
<div
v-if="currentMessages.length > 0"
class="history-indicator"
@click="loadMoreHistory"
style="
cursor: pointer;
text-align: center;
color: #409eff;
margin-bottom: 10px;
"
>
<i class="el-icon-arrow-up"></i>
<span>加载更多历史消息</span>
</div>
<div v-if="messagesLoading" class="cs-loading">
<i class="el-icon-loading"></i>
<p>加载消息中...</p>
</div>
<div v-else-if="currentMessages.length === 0" class="cs-empty-chat">
<i class="el-icon-chat-line-round"></i>
<p>暂无消息记录</p>
</div>
<div v-else class="cs-message-list">
<div
v-for="(message, idx) in currentMessages"
:key="idx"
class="cs-message"
:class="{ 'cs-message-self': message.isSelf }"
>
<div class="cs-message-time" v-if="showMessageTime(idx)">
{{ formatTime(message.time) }}
</div>
<div class="cs-message-content">
<div class="cs-avatar">
<i class="iconfont icon-icon28" style="font-size: 2vw"></i>
<!-- <el-avatar
:size="36"
:src="
message.isSelf
? getDefaultAvatar('我')
: getDefaultAvatar(message.sender)
"
>
{{ message.sender ? message.sender.charAt(0) : "?" }}
</el-avatar> -->
</div>
<div class="cs-bubble">
<div class="cs-sender">{{ message.sender }}</div>
<div v-if="!message.isImage" class="cs-text">
{{ message.content }}
</div>
<div v-else class="cs-image">
<img
:src="message.content"
@click="previewImage(message.content)"
/>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- 输入区域 -->
<div class="cs-chat-input">
<div class="cs-toolbar">
<i
class="el-icon-picture-outline"
title="发送图片"
@click="openImageUpload"
></i>
<input
type="file"
ref="imageInput"
accept="image/*"
style="display: none"
@change="handleImageUpload"
/>
<!-- <i class="el-icon-folder-opened" title="发送文件"></i> -->
<!-- <i class="el-icon-s-opportunity" title="发送表情"></i> -->
<!-- <i class="el-icon-scissors" title="截图"></i> -->
</div>
<!-- @keydown.enter.prevent="sendMessage" -->
<div class="cs-input-area">
<el-input
type="textarea"
v-model="inputMessage"
:rows="3"
:disabled="!currentContact"
placeholder="请输入消息按Enter键发送按Ctrl+Enter键换行"
@keydown.enter.native="handleKeyDown"
></el-input>
</div>
<div class="cs-send-area">
<span class="cs-counter">{{ inputMessage.length }}/500</span>
<el-button
type="primary"
:disabled="!currentContact || !inputMessage.trim() || sending"
@click="sendMessage"
>
<i v-if="sending" class="el-icon-loading"></i>
<span v-else>发送</span>
</el-button>
</div>
</div>
</div>
</div>
<!-- 图片预览 -->
<el-dialog
:visible.sync="previewVisible"
append-to-body
class="image-preview-dialog"
>
<img :src="previewImageUrl" class="preview-image" alt="预览图片" />
</el-dialog>
</div>
</template>
<script>
import {
getRoomList,
getHistory,
getHistory7,
getReadMessage,
getUpdateRoom,
getFileUpdate,
} from "../../api/customerService";
// 正确导入 Client
import { Client, Stomp } from "@stomp/stompjs";
export default {
name: "CustomerServiceChat",
data() {
return {
searchText: "",
inputMessage: "",
currentContactId: null,
previewVisible: false,
previewImageUrl: "",
contacts: [],
messages: {},
messagesLoading: false,
sending: false,
loadingRooms: true,
stompClient: null,
wsConnected: false,
userEmail: "497681109@qq.com", // 当前客服邮箱
userType: 1, // 0或者1 游客或者登录用户
loadingHistory: false, // 是否正在加载历史消息
userViewHistory: false, // 用户是否在浏览历史
userScrolled: false, // 新增:用户是否手动滚动过
history7Params: {
//7天历史消息参数
id: "", //最后一条消息id
roomId: "", //聊天室id
userType: 2, //用户类型
email: "497681109@qq.com", //客服邮箱
},
historyAllParams: {
//7天以前的历史消息
id: "", //最后一条消息id
roomId: "", //聊天室id
userType: 2, //用户类型
},
receiveUserType: "", //接收者类型
manualCreatedRooms: [], //手动创建的聊天室
chatRooms: [], // 初始化聊天室列表数组
isWebSocketConnected: false,
connectionStatus: "disconnected",
};
},
computed: {
filteredContacts() {
//搜索联系人
if (!this.searchText) {
return this.contacts;
}
return this.contacts.filter((contact) =>
contact.name.toLowerCase().includes(this.searchText.toLowerCase())
);
},
currentContact() {
//选中联系人对象
return this.contacts.find(
(contact) => contact.roomId === this.currentContactId
);
},
currentMessages() {
//当前聊天室消息
return this.messages[this.currentContactId] || [];
},
},
async created() {
try {
// 初始化用户信息
// this.determineUserType();
// 获取聊天室列表
await this.fetchRoomList();
// 默认选择第一个联系人
if (this.contacts.length > 0) {
this.selectContact(this.contacts[0].roomId);
}
// 在组件创建时加载手动创建的聊天室
this.loadManualCreatedRooms();
// 初始化 WebSocket 连接
this.initWebSocket();
} catch (error) {
console.error("初始化失败:", error);
}
},
async mounted() {
// 获取聊天室列表
await this.fetchRoomList();
// 默认选择第一个联系人
if (this.contacts.length > 0) {
this.selectContact(this.contacts[0].roomId);
}
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
window.addEventListener("setItem", () => {
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
});
// 确保初始加载后滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
// 添加滚动事件监听
this.$nextTick(() => {
if (this.$refs.messageContainer) {
this.$refs.messageContainer.addEventListener(
"scroll",
this.handleScroll
);
}
});
},
methods: {
handleKeyDown(e) {
console.log("e:hhhhshshsshshshhh好的好的和", e);
// 如果按住了 Ctrl 键,则不发送消息(换行)
if (e.ctrlKey) {
return;
}
// 阻止默认行为
e.preventDefault();
// 发送消息
this.sendMessage();
},
// 初始化 WebSocket 连接
initWebSocket() {
if (this.isWebSocketConnected) return;
try {
const wsUrl = `${process.env.VUE_APP_BASE_API}chat/ws`;
this.stompClient = Stomp.client(wsUrl);
// 配置 STOMP 客户端参数
this.stompClient.splitLargeFrames = true; // 启用大型消息帧分割
// 修改调试日志的方式
this.stompClient.debug = (str) => {
// 只打印与客服相关的日志
if (
str.includes("CONNECTED") ||
str.includes("DISCONNECTED") ||
str.includes("ERROR")
) {
console.log("[客服系统]", str);
}
};
const headers = {
email: this.userEmail,
type: this.userType,
};
this.stompClient.connect(
headers,
(frame) => {
console.log("[客服系统] WebSocket 连接成功", frame);
this.isWebSocketConnected = true;
this.connectionStatus = "connected";
this.subscribeToMessages();
},
(error) => {
console.error("[客服系统] WebSocket 错误:", error);
this.handleDisconnect();
}
);
// 配置心跳
this.stompClient.heartbeat.outgoing = 20000;
this.stompClient.heartbeat.incoming = 20000;
} catch (error) {
console.error("初始化 CustomerService WebSocket 失败:", error);
this.handleDisconnect();
}
},
// // 订阅消息
subscribeToMessages() {
if (!this.stompClient || !this.isWebSocketConnected) return;
try {
// 修改订阅路径,使用客服特定的订阅路径
this.stompClient.subscribe(
`/sub/queue/customer/${this.userEmail}`,
this.handleIncomingMessage,
{
id: `customer_${this.userEmail}`,
}
);
// this.stompClient.subscribe(
// `/user/queue/customer_service`, // 修改为客服专用的订阅路径
// this.handleIncomingMessage,
// {
// id: `customer_service_${this.userEmail}`,
// }
// );
console.log(
"CustomerService 成功订阅消息频道:",
`/sub/queue/customer/${this.userEmail}`
);
} catch (error) {
console.error("CustomerService 订阅消息失败:", error);
}
},
// 断开连接
disconnectWebSocket() {
if (this.stompClient) {
try {
// 取消所有订阅
if (this.stompClient.subscriptions) {
Object.keys(this.stompClient.subscriptions).forEach((id) => {
this.stompClient.unsubscribe(id);
});
}
// 断开连接
this.stompClient.deactivate();
this.isWebSocketConnected = false;
this.connectionStatus = "disconnected";
} catch (error) {
console.error("断开 CustomerService WebSocket 连接失败:", error);
}
}
},
// 处理断开连接
handleDisconnect() {
this.isWebSocketConnected = false;
this.connectionStatus = "error";
// 尝试重新连接
setTimeout(() => {
if (!this.isWebSocketConnected) {
this.initWebSocket();
}
}, 5000);
},
// 获取当前的 UTC 时间
getUTCTime() {
const now = new Date();
return new Date(now.getTime() + now.getTimezoneOffset() * 60000);
},
// 发送消息
async sendMessage() {
if (!this.inputMessage.trim() || !this.currentContact || this.sending)
return;
const messageContent = this.inputMessage.trim();
this.inputMessage = "";
this.sending = true;
try {
// 判断接收者类型
if (this.currentContact.sendUserType !== undefined) {
// 如果存在 sendUserType,使用它作为接收者类型
this.receiveUserType = this.currentContact.sendUserType;
} else {
// 默认为登录用户类型 1
this.receiveUserType = 1;
}
const message = {
content: messageContent,
type: 1, // 1 表示文字消息
email: this.currentContact.name,
receiveUserType: this.receiveUserType,
roomId: this.currentContactId,
};
// 发送消息
this.stompClient.send(
"/point/send/message/to/user",
{},
JSON.stringify(message)
);
// 添加到本地消息列表
this.addMessageToChat({
sender: "我",
content: messageContent,
time: new Date(),
isSelf: true,
isImage: false,
roomId: this.currentContactId,
type: 1,
});
// 重置当前聊天室的未读消息数
const contact = this.contacts.find(
(c) => c.roomId === this.currentContactId
);
if (contact) {
contact.unread = 0;
}
} catch (error) {
console.error("发送消息失败:", error);
this.$message.error("发送消息失败,请重试");
} finally {
this.sending = false;
}
},
// 订阅个人消息队列
subscribeToPersonalMessages() {
if (!this.stompClient || !this.wsConnected) return;
this.stompClient.subscribe(
`/user/queue/${this.userEmail}`,
this.handleIncomingMessage
);
},
// 处理接收到的消息
async handleIncomingMessage(message) {
try {
const msg = JSON.parse(message.body);
console.log("客服收到的消息", msg);
const messageData = {
id: msg.id,
sender: msg.sendEmail,
avatar:
msg.sendUserType === 2
? "iconfont icon-icon28"
: "iconfont icon-user",
content: msg.content,
time: new Date(msg.sendTime),
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,
};
// 更新或创建聊天室
const existingContact = this.contacts.find(
(c) => c.roomId === messageData.roomId
);
if (!existingContact) {
// 如果聊天室不存在,创建新的聊天室
const newContact = {
roomId: messageData.roomId,
name: messageData.sender,
lastMessage: messageData.isImage ? "[图片]" : messageData.content,
lastTime: messageData.time,
unread: 1,
important: false,
isGuest: messageData.sendUserType === 0,
sendUserType: messageData.sendUserType,
isManualCreated: true,
};
this.contacts.push(newContact);
this.$set(this.messages, messageData.roomId, []);
} else {
// 如果聊天室已存在,更新最后一条消息
existingContact.lastMessage = messageData.isImage ? "[图片]" : messageData.content;
existingContact.lastTime = messageData.time;
}
// 添加消息到聊天记录
if (!this.messages[messageData.roomId]) {
this.$set(this.messages, messageData.roomId, []);
}
this.messages[messageData.roomId].push({
id: messageData.id,
sender: messageData.sender,
avatar: messageData.avatar,
content: messageData.content,
time: messageData.time,
isSelf: messageData.isSelf,
isImage: messageData.isImage,
type: messageData.type,
roomId: messageData.roomId,
});
// 如果是当前选中的聊天室,标记为已读
if (messageData.roomId === this.currentContactId) {
this.markMessagesAsRead(messageData.roomId);
} else {
// 更新未读消息数
const contact = this.contacts.find((c) => c.roomId === messageData.roomId);
if (contact) {
contact.unread = (contact.unread || 0) + 1;
}
}
// 重新排序联系人列表
this.sortContacts();
} catch (error) {
console.error("处理新消息失败:", error);
}
},
// 处理新聊天室创建
handleNewChatRoom(messageData) {
// 检查是否已存在该聊天室
const existingContact = this.contacts.find(
(c) => c.roomId === messageData.roomId
);
if (!existingContact) {
// 创建新的聊天室对象
const newContact = {
roomId: messageData.roomId,
name: messageData.sender,
// 修改这里:使用实际收到的消息内容作为最后一条消息
lastMessage: messageData.isImage ? "[图片]" : messageData.content,
lastTime: messageData.time,
unread: 1,
important: false,
isGuest: messageData.sendUserType === 0,
sendUserType: messageData.sendUserType,
isManualCreated: true,
clientReadNum: messageData.clientReadNum,
};
// 添加到聊天列表
this.contacts.push(newContact);
// 初始化该聊天室的消息数组
this.$set(this.messages, messageData.roomId, []);
// 将当前消息添加到聊天记录中
this.messages[messageData.roomId].push({
id: messageData.id,
sender: messageData.sender,
avatar:
messageData.sendUserType === 2
? "iconfont icon-icon28"
: "iconfont icon-user",
content: messageData.content,
time: messageData.time,
isSelf: false,
isImage: messageData.type === 2,
type: messageData.type,
roomId: messageData.roomId,
});
// 保存到手动创建的聊天室列表
this.manualCreatedRooms.push(newContact);
this.saveManualCreatedRooms();
// 重新排序联系人列表
this.sortContacts();
}
},
saveManualCreatedRooms() {
localStorage.setItem(
"manualCreatedRooms",
JSON.stringify(this.manualCreatedRooms)
);
},
// 从 localStorage 加载手动创建的聊天室
async loadManualCreatedRooms() {
try {
const savedRooms = localStorage.getItem("manualCreatedRooms");
if (savedRooms) {
this.manualCreatedRooms = JSON.parse(savedRooms);
// 将手动创建的聊天室添加到当前联系人列表
for (const room of this.manualCreatedRooms) {
const exists = this.contacts.find((c) => c.roomId === room.roomId);
if (!exists) {
this.contacts.push({
...room,
lastTime: new Date(room.lastTime),
});
// 初始化消息数组
if (!this.messages[room.roomId]) {
this.$set(this.messages, room.roomId, []);
// 如果需要,可以在这里加载该聊天室的历史消息
await this.loadMessages(room.roomId);
}
}
}
this.sortContacts();
}
} catch (error) {
console.error("加载手动创建的聊天室失败:", error);
}
},
// 添加新方法:创建新聊天室
async createNewChatRoom(messageData) {
try {
// 调用后端 API 创建新的聊天室
const response = await createChatRoom({
userEmail: messageData.sender,
userType: messageData.sendUserType,
});
if (response && response.code === 200) {
const newRoom = {
userEmail: messageData.sender,
roomId: response.data.roomId,
lastMessage: messageData.content,
lastMessageTime: messageData.time,
unreadCount: messageData.clientReadNum || 0,
userType: messageData.sendUserType,
};
this.chatRooms.unshift(newRoom);
return newRoom;
}
} catch (error) {
console.error("创建新聊天室失败:", error);
throw error;
}
},
// 更新聊天室列表
updateChatRoomList(messageData) {
const roomIndex = this.chatRooms.findIndex(
(room) => room.roomId === messageData.roomId
);
if (roomIndex !== -1) {
// 更新现有聊天室信息
this.chatRooms[roomIndex] = {
...this.chatRooms[roomIndex],
lastMessage: messageData.content,
lastMessageTime: messageData.time,
unreadCount:
messageData.clientReadNum || this.chatRooms[roomIndex].unreadCount,
};
// 将更新的聊天室移到列表顶部
const updatedRoom = this.chatRooms.splice(roomIndex, 1)[0];
this.chatRooms.unshift(updatedRoom);
}
},
// 修改标记为已读方法,添加参数支持
async markMessagesAsRead(roomId = this.currentContactId) {
if (!roomId) return;
try {
const data = {
roomId: roomId,
userType: 2,
};
const response = await getReadMessage(data);
if (response && response.code === 200) {
console.log("消息已标记为已读");
// 更新联系人列表中的未读计数
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact) {
contact.unread = 0;
}
} else {
console.warn("标记消息已读失败", response);
}
} catch (error) {
console.error("标记消息已读出错:", error);
}
},
// 解析 UTC 时间字符串
parseUTCTime(timeStr) {
if (!timeStr) return new Date(); // 如果没有时间,返回当前时间
try {
return new Date(timeStr); // 直接解析 UTC 时间
} catch (error) {
console.error("解析时间错误:", error);
return new Date();
}
},
// 获取聊天室列表
async fetchRoomList() {
try {
this.loadingRooms = true;
const response = await getRoomList();
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;
return {
roomId: room.id,
name: room.userEmail || "未命名聊天室",
avatar: this.getDefaultAvatar(room.roomName || "未命名聊天室"),
// 修改这里:优先使用服务器返回的消息,其次是现有联系人的消息
lastMessage: room.lastMessage || (existingContact ? existingContact.lastMessage : "暂无消息"),
lastTime: room.lastUserSendTime
? new Date(room.lastUserSendTime)
: new Date(),
unread: existingContact?.unread ?? room.unreadCount ?? 0,
important: isImportant,
isManualCreated: manualRoom ? true : false,
sendUserType: room.sendUserType,
isGuest: room.sendUserType === 0,
};
});
// 更新联系人列表
this.contacts = newContacts;
this.sortContacts();
}
} catch (error) {
console.error("获取聊天室列表异常:", error);
this.$message.error("获取聊天室列表异常");
} finally {
this.loadingRooms = false;
}
},
// 加载更多历史消息
async loadMoreHistory() {
if (!this.currentContactId) return;
// 获取当前已加载的消息列表
const currentMsgs = this.messages[this.currentContactId] || [];
// 取最后一条消息的id
const lastMsg =
currentMsgs.length > 0 ? currentMsgs[currentMsgs.length - 1] : null;
this.history7Params.id = lastMsg ? lastMsg.id : "";
// this.history7Params.pageNum += 1; // 递增页码
this.history7Params.roomId = this.currentContactId;
this.history7Params.email = this.userEmail;
try {
this.messagesLoading = true;
const response = await getHistory7(this.history7Params);
if (response && response.code === 200 && response.data) {
let moreMessages = response.data
.filter((msg) => msg.roomId === this.currentContactId)
.map((msg) => ({
id: msg.id,
sender: msg.isSelf === 1 ? "我" : msg.sendEmail || "未知发送者",
avatar: "iconfont icon-icon28",
content: msg.content,
time: new Date(msg.createTime),
isSelf: msg.isSelf === 1,
isImage: msg.type === 2,
isRead: msg.isRead === 1,
type: msg.type,
roomId: msg.roomId,
}));
moreMessages = moreMessages.sort((a, b) => a.time - b.time);
// 追加到当前消息列表前面
const oldMessages = this.messages[this.currentContactId] || [];
this.$set(this.messages, this.currentContactId, [
...moreMessages,
...oldMessages,
]);
} else {
this.$message.warning("没有更多历史消息");
}
} catch (error) {
this.$message.error("加载更多历史消息失败");
} finally {
this.messagesLoading = false;
}
},
// 选择联系人
async selectContact(roomId) {
if (this.currentContactId === roomId) return;
try {
this.messagesLoading = true; // 显示加载状态
this.currentContactId = roomId;
this.userViewHistory = false;
// 重置分页参数
this.history7Params = {
id: "", // 首次加载为空
// pageNum: 1, // 首次页码为1
roomId: roomId,
userType: 2,
};
// 加载历史消息
await this.loadMessages(roomId);
// 标记消息为已读
await this.markMessagesAsRead(roomId);
} catch (error) {
console.error("选择联系人失败:", error);
this.$message.error("加载聊天记录失败");
} finally {
this.messagesLoading = false;
// 滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
}
},
//判断是否在聊天框底部
isAtBottom() {
const container = this.$refs.messageContainer;
if (!container) return true;
// 允许2px误差
return (
container.scrollHeight - container.scrollTop - container.clientHeight <
2
);
},
// 加载聊天消息
async loadMessages(roomId) {
if (!roomId) return;
try {
this.history7Params.email = this.userEmail;
this.history7Params.roomId = roomId;
const response = await getHistory7(this.history7Params);
if (response?.code === 200 && response.data) {
// 处理消息数据
let roomMessages = response.data
.filter((msg) => msg.roomId == roomId)
.map((msg) => ({
id: msg.id,
sender: msg.isSelf === 1 ? "我" : msg.sendEmail || "未知发送者",
avatar:
msg.sendUserType == 2
? "iconfont icon-icon28"
: "iconfont icon-user",
content: msg.content,
time: new Date(msg.createTime),
isSelf: msg.isSelf === 1,
isImage: msg.type === 2,
isRead: msg.isRead === 1,
type: msg.type,
roomId: msg.roomId,
sendUserType: msg.sendUserType,
}));
// 按时间排序
roomMessages = roomMessages.sort((a, b) => a.time - b.time);
// 更新消息列表
this.$set(this.messages, roomId, roomMessages);
// 更新联系人的未读状态
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact) {
contact.unread = 0;
}
} else {
// 如果没有消息数据,初始化为空数组
this.$set(this.messages, roomId, []);
if (response?.code !== 200) {
this.$message.warning("获取聊天记录失败");
}
}
} catch (error) {
console.error("加载消息异常:", error);
this.$message.error("加载消息异常");
this.$set(this.messages, roomId, []);
}
},
// 添加消息到聊天记录
addMessageToChat(messageData) {
const roomId = messageData.roomId || this.currentContactId;
// 如果该聊天室的消息数组不存在,则初始化
if (!this.messages[roomId]) {
this.$set(this.messages, roomId, []);
}
// 构造消息对象
const message = {
id: messageData.id || Date.now(), // 如果没有id则使用时间戳
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,
};
// 添加消息到数组
this.messages[roomId].push(message);
// 更新最后一条消息
this.updateContactLastMessage({
roomId: roomId,
content: message.isImage ? "[图片]" : message.content,
isImage: message.isImage,
});
// 如果是当前聊天室且用户在底部,滚动到底部
if (roomId === this.currentContactId && !this.userViewHistory) {
this.$nextTick(() => {
this.scrollToBottom();
});
}
},
// 处理图片上传
async handleImageUpload(event) {
// 检查是否有选中的联系人和 WebSocket 连接
if (!this.currentContact) {
this.$message.warning("请先选择联系人");
return;
}
if (!this.stompClient || !this.isWebSocketConnected) {
this.$message.warning("聊天连接已断开,请刷新页面重试");
return;
}
const file = event.target.files[0];
if (!file) return;
// 检查是否为图片
if (!file.type.startsWith("image/")) {
this.$message.warning("只能上传图片文件!");
return;
}
// 检查文件大小 (限制为5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
this.$message.warning("图片大小不能超过5MB!");
return;
}
this.sending = true;
try {
// 将图片转换为 base64
const reader = new FileReader();
reader.onload = (e) => {
const base64Image = e.target.result;
// 构建WebSocket消息对象
const message = {
content: base64Image,
type: 2, // 类型2表示图片消息
email: this.currentContact.name,
receiveUserType: this.currentContact.sendUserType || 1, // 使用联系人的用户类型
roomId: this.currentContactId,
};
// 发送WebSocket消息
this.stompClient.send(
"/point/send/message/to/user",
{},
JSON.stringify(message)
);
// 添加到本地消息列表
this.addMessageToChat({
sender: "我",
content: base64Image,
time: new Date(),
isSelf: true,
isImage: true,
type: 2,
roomId: this.currentContactId,
});
// 更新联系人最后一条消息
this.updateContactLastMessage({
roomId: this.currentContactId,
content: "[图片]",
isImage: true
});
this.$message.success("图片已发送");
};
reader.onerror = () => {
this.$message.error("图片读取失败");
this.sending = false;
};
// 开始读取文件
reader.readAsDataURL(file);
} catch (error) {
console.error("上传图片异常:", error);
this.$message.error("上传图片失败,请重试");
} finally {
this.sending = false;
// 清空文件选择器
this.$refs.imageInput.value = "";
}
},
// 更新联系人最后一条消息
updateContactLastMessage(message) {
const contact = this.contacts.find((c) => c.roomId === message.roomId);
if (contact) {
contact.lastMessage = message.isImage ? "[图片]" : message.content;
contact.lastTime = new Date(); // 使用当前时间或消息时间
// 重新排序联系人列表
this.sortContacts();
}
},
// 增加未读消息计数
incrementUnreadCount(roomId, readNum = 1) {
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact) {
// 如果有指定未读数直接设置否则增加1
if (readNum > 1) {
contact.unread = readNum;
} else {
contact.unread = (contact.unread || 0) + 1;
}
}
},
// 预览图片
previewImage(imageUrl) {
this.previewImageUrl = imageUrl;
this.previewVisible = true;
},
// 标记聊天为重要/非重要
async toggleImportant(roomId, important) {
if (!roomId) return;
try {
// 发送请求,使用 id 和 flag 参数
const response = await getUpdateRoom({
id: roomId,
flag: important ? 1 : 0, // 1代表重要0代表不重要
});
if (response && response.code === 200) {
// 更新本地数据
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact) {
contact.important = important;
}
// 重新排序联系人列表,使重要的排在前面
this.sortContacts();
this.$message.success(important ? "已标记为重要" : "已取消重要标记");
} else {
this.$message.error(response?.msg || "标记操作失败");
}
} catch (error) {
console.error("标记聊天状态异常:", error);
this.$message.error("操作失败,请重试");
}
},
// 根据重要性对联系人列表排序
sortContacts() {
this.contacts.sort((a, b) => {
// 首先按重要性排序(重要的在前)
if (a.important && !b.important) return -1;
if (!a.important && b.important) return 1;
// 确保日期转换正确
const aTime =
a.lastTime instanceof Date ? a.lastTime : new Date(a.lastTime || 0);
const bTime =
b.lastTime instanceof Date ? b.lastTime : new Date(b.lastTime || 0);
// 其次按最后消息时间排序(最新的在前)
return bTime.getTime() - aTime.getTime();
});
},
// 滚动到底部
scrollToBottom() {
const container = this.$refs.messageContainer;
if (!container) return;
// 使用平滑滚动
container.scrollTo({
top: container.scrollHeight,
behavior: "smooth",
});
},
// 判断是否显示时间
showMessageTime(index) {
// 显示时间的逻辑第一条消息或距离上一条消息超过5分钟
if (index === 0) return true;
const currentMsg = this.currentMessages[index];
const prevMsg = this.currentMessages[index - 1];
if (!currentMsg.time || !prevMsg.time) return false;
const currentTime = new Date(currentMsg.time).getTime();
const prevTime = new Date(prevMsg.time).getTime();
const diffMinutes = (currentTime - prevTime) / (1000 * 60);
return diffMinutes > 5;
},
// 格式化消息时间
formatTime(date) {
if (!date) return "";
if (!(date instanceof Date)) {
date = new Date(date);
}
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const isToday = today.toDateString() === date.toDateString();
const isYesterday = yesterday.toDateString() === date.toDateString();
if (isToday) {
return `今天 ${date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}`;
} else if (isYesterday) {
return `昨天 ${date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}`;
} else {
return `${date.getFullYear()}/${
date.getMonth() + 1
}/${date.getDate()} ${date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}`;
}
},
// 格式化最后消息时间
formatLastTime(date) {
if (!date) return "";
if (!(date instanceof Date)) {
date = new Date(date);
}
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const isToday = today.toDateString() === date.toDateString();
const isYesterday = yesterday.toDateString() === date.toDateString();
if (isToday) {
return date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
} else if (isYesterday) {
return "昨天";
} else {
return `${date.getMonth() + 1}/${date.getDate()}`;
}
},
// 获取默认头像
getDefaultAvatar(name) {
if (!name) return "";
// 根据名称生成颜色
const colors = [
"#4CAF50",
"#9C27B0",
"#FF5722",
"#2196F3",
"#FFC107",
"#607D8B",
"#E91E63",
];
const index = Math.abs(name.charCodeAt(0)) % colors.length;
const color = colors[index];
// 获取名称首字母
const initial = name.charAt(0).toUpperCase();
return initial;
// 生成占位头像URL
},
// 处理滚动事件
handleScroll() {
// 如果滚动到底部,重置标记
if (this.isAtBottom()) {
this.userViewHistory = false;
} else {
// 用户滚动到其他位置,标记为正在查看历史
this.userViewHistory = true;
}
},
// 小时钟加载历史消息(7天前)
async loadHistory() {
this.loadingHistory = true;
this.userViewHistory = true; // 用户主动查看历史
console.log("哇哈哈哈哈哈哈", this.currentContactId);
if (!this.currentContactId) return;
try {
this.messagesLoading = true;
// 获取当前已加载的消息列表
const currentMsgs = this.messages[this.currentContactId] || [];
// 取最后一条消息的id
const lastMsg =
currentMsgs.length > 0 ? currentMsgs[currentMsgs.length - 1] : null;
this.history7Params.id = lastMsg ? lastMsg.id : "";
// this.history7Params.pageNum += 1; // 递增页码
this.history7Params.roomId = this.currentContactId;
this.history7Params.email = this.userEmail;
const response = await getHistory7(this.history7Params);
if (response && response.code === 200 && response.data) {
let historyMessages = response.data
.filter((msg) => msg.roomId === this.currentContactId)
.map((msg) => ({
id: msg.id,
sender: msg.sendEmail,
avatar: "iconfont icon-icon28",
content: msg.content,
time: new Date(msg.createTime),
isSelf: msg.isSelf === 1,
isImage: msg.type === 2,
isRead: msg.isRead === 1,
type: msg.type,
roomId: msg.roomId,
}));
historyMessages = historyMessages.sort((a, b) => a.time - b.time);
const currentMessages = this.messages[this.currentContactId] || [];
this.$set(this.messages, this.currentContactId, [
...historyMessages,
...currentMessages,
]);
this.$message.success("已加载历史消息");
} else {
this.$message.warning("没有更多历史消息");
}
} catch (error) {
console.error("加载历史消息异常:", error);
this.$message.error("加载历史消息异常");
} finally {
this.messagesLoading = false;
this.loadingHistory = false;
}
},
// 刷新当前聊天消息
async refreshMessages() {
if (!this.currentContactId) return;
await this.loadMessages(this.currentContactId);
},
// 打开图片上传控件
openImageUpload() {
if (!this.currentContact) return;
this.$refs.imageInput.click();
},
},
beforeDestroy() {
if (this.stompClient) {
if (this.stompClient.connected) {
this.stompClient.disconnect(() => {
console.log("WebSocket 已断开连接");
});
}
}
// 组件销毁前断开连接
this.disconnectWebSocket();
// if (this.roomListInterval) {
// clearInterval(this.roomListInterval);
// }
// 移除滚动事件监听
if (this.$refs.messageContainer) {
this.$refs.messageContainer.removeEventListener(
"scroll",
this.handleScroll
);
}
},
};
</script>
<style scoped>
.cs-chat-container {
width: 70%;
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;
}
.cs-chat-wrapper {
display: flex;
height: 100%;
}
/* 联系人列表样式 */
.cs-contact-list {
width: 260px;
border-right: 1px solid #e0e0e0;
background-color: #fff;
display: flex;
flex-direction: column;
height: 100%;
}
.cs-header {
padding: 15px;
font-weight: bold;
border-bottom: 1px solid #e0e0e0;
color: #333;
font-size: 16px;
background-color: #f8f8f8;
}
.cs-search {
padding: 10px;
border-bottom: 1px solid #e0e0e0;
}
.cs-contacts {
flex: 1;
overflow-y: auto;
}
.cs-contact-item {
display: flex;
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.cs-contact-item:hover {
background-color: #f5f5f5;
}
.cs-contact-item.active {
background-color: #e6f7ff;
}
.cs-avatar {
position: relative;
margin-right: 10px;
}
.unread-badge {
position: absolute;
top: -5px;
right: -5px;
background-color: #f56c6c;
color: white;
font-size: 12px;
min-width: 16px;
height: 16px;
text-align: center;
line-height: 16px;
border-radius: 8px;
padding: 0 4px;
}
.cs-contact-info {
flex: 1;
min-width: 0;
}
.cs-contact-name {
font-weight: 500;
font-size: 14px;
color: #333;
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.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;
}
.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;
}
.cs-loading,
.cs-empty-chat {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #909399;
}
.cs-loading i,
.cs-empty-chat i {
font-size: 60px;
margin-bottom: 20px;
color: #dcdfe6;
}
.cs-message-list {
display: flex;
flex-direction: column;
}
.cs-message {
margin-bottom: 15px;
}
.cs-message-time {
text-align: center;
margin: 10px 0;
font-size: 12px;
color: #909399;
}
.cs-message-content {
display: flex;
align-items: flex-start;
}
.cs-message-self .cs-message-content {
flex-direction: row-reverse;
}
.cs-message-self .cs-avatar {
margin-right: 0;
margin-left: 10px;
}
.cs-bubble {
max-width: 70%;
padding: 8px 12px;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
position: relative;
}
.cs-message-self .cs-bubble {
background-color: #d8f4fe;
}
.cs-sender {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.cs-text {
font-size: 14px;
line-height: 1.5;
word-break: break-word;
}
.cs-image img {
max-width: 200px;
max-height: 200px;
border-radius: 4px;
cursor: pointer;
}
/* 输入区域样式 */
.cs-chat-input {
padding: 10px;
background-color: #fff;
border-top: 1px solid #e0e0e0;
}
.cs-toolbar {
padding: 5px 0;
margin-bottom: 5px;
}
.cs-toolbar i {
font-size: 18px;
margin-right: 15px;
color: #606266;
cursor: pointer;
}
.cs-toolbar i:hover {
color: #409eff;
}
.cs-input-area {
margin-bottom: 10px;
}
.cs-send-area {
display: flex;
justify-content: flex-end;
align-items: center;
}
.cs-counter {
margin-right: 10px;
font-size: 12px;
color: #909399;
}
.shop-type {
font-size: 12px;
color: #909399;
font-weight: normal;
margin-left: 5px;
}
/* 图片预览 */
.image-preview-dialog {
display: flex;
justify-content: center;
align-items: center;
}
.preview-image {
max-width: 100%;
max-height: 80vh;
}
/* 响应式调整 */
@media (max-width: 768px) {
.cs-contact-list {
width: 200px;
}
.cs-image img {
max-width: 150px;
max-height: 150px;
}
}
/* 添加重要标记样式 */
.important-star {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
cursor: pointer;
margin-left: 5px;
color: #c0c4cc; /* 默认灰色 */
transition: color 0.3s;
}
.important-star:hover {
color: #ac85e0; /* 鼠标悬停时的颜色 */
}
.important-star.is-important {
color: #ac85e0; /* 重要标记时的紫色 */
}
.important-star i {
font-size: 16px;
}
/* 调整联系人项目布局 */
.cs-contact-item {
display: flex;
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
align-items: center; /* 确保垂直居中 */
}
/* 重要标签的样式调整 */
.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;
}
</style>