.m2pool 客服系统对接 1.订阅接口区分用户和客服 避免消息混乱 已处理 2.历史消息渲染isSelf返回值有问题 已处理 3.图片发送方式修改为用base64Image发送后soket自动断开 已处理 4.优化心跳机制 断线重连 最多自动重连5次 已处理 5.游客功能添加、删除列表离线游客 处理中

6.客服给游客发送消息 游客收不到推送消息 已处理
This commit is contained in:
yaoqin 2025-05-16 14:01:38 +08:00
parent 2e56d71b0c
commit a326f62f81
6 changed files with 1203 additions and 769 deletions

View File

@ -5,8 +5,8 @@ VUE_APP_TITLE = m2pool
ENV = 'development'
#开发环境
VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
# VUE_APP_BASE_API = 'http://10.168.2.150:8101/'
# VUE_APP_BASE_API = 'https://test.m2pool.com/api/'
VUE_APP_BASE_API = 'http://10.168.2.150:8101/'
VUE_APP_BASE_URL = 'https://test.m2pool.com/'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@ -58,11 +58,11 @@ export function getUpdateRoom(data) {
//图根据当前用户邮箱查询聊天室id
export function getUserid() {
export function getUserid(data) {
return request({
url: `chat/rooms/find/room/by/userid`,
method: 'get',
method: 'post',
data
})
}

View File

@ -220,10 +220,16 @@ export default {
cachedMessages: {}, //
isMinimized: false, //
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectInterval: 5000, // 5
isReconnecting: false,
};
},
async created() {
this.determineUserType();
//
await this.initChatSystem();
},
@ -247,7 +253,7 @@ export default {
async initChatSystem() {
try {
// ID
const userData = await this.fetchUserid();
const userData = await this.fetchUserid({ email: this.userEmail });
if (userData) {
this.roomId = userData.id;
this.receivingEmail = userData.userEmail;
@ -273,7 +279,7 @@ export default {
determineUserType() {
try {
const token = localStorage.getItem("token");
console.log("token", token);
if (!token) {
//
this.userType = 0;
@ -293,11 +299,13 @@ export default {
if (userInfo.roleKey === "customer_service") {
//
this.userType = 2;
this.userEmail = "";
} else {
//
this.userType = 1;
}
this.userEmail = email;
}
} catch (parseError) {
console.error("解析用户信息失败:", parseError);
//
@ -323,51 +331,80 @@ export default {
try {
//
this.stompClient.subscribe(
`/user/queue/${this.userEmail}`,
`/sub/queue/user/${this.userEmail}`,
this.onMessageReceived,
{
id: `chat_${this.userEmail}`,
}
// {
// id: `chat_${this.userEmail}`,
// }
);
console.log("成功订阅消息频道:", `/user/queue/${this.userEmail}`);
console.log("成功订阅消息频道:", `/sub/queue/user/${this.userEmail}`);
} catch (error) {
console.error("订阅消息失败:", error);
this.$message.error("消息订阅失败,可能无法接收新消息");
}
},
// WebSocket
connectWebSocket() {
if (this.isWebSocketConnected) return;
if (this.isWebSocketConnected || this.isReconnecting) return;
this.connectionStatus = "connecting";
this.isReconnecting = true;
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) => {
// console.log("STOMP Debug:", str);
// //
// if (str.includes("CONNECTED")) {
// console.log("WebSocket ");
// this.isWebSocketConnected = true;
// this.connectionStatus = "connected";
// this.reconnectAttempts = 0;
// this.isReconnecting = false;
// } else if (
// str.includes("DISCONNECTED") ||
// str.includes("Connection closed")
// ) {
// console.log("WebSocket ");
// this.handleDisconnect();
// }
// };
const headers = {
email: this.userEmail,
type: this.userType,
};
//
this.stompClient.onStompError = (frame) => {
console.error("STOMP 错误:", frame);
this.handleDisconnect();
};
this.stompClient.connect(
headers,
(frame) => {
console.log("WebSocket Connected:", frame);
this.isWebSocketConnected = true;
this.connectionStatus = "connected";
this.reconnectAttempts = 0;
this.isReconnecting = false;
//
this.subscribeToPersonalMessages();
//
this.$message.success("连接成功");
},
(error) => {
console.error("WebSocket Error:", error);
this.isWebSocketConnected = false;
this.connectionStatus = "error";
//
setTimeout(() => this.connectWebSocket(), 5000);
this.handleDisconnect();
}
);
@ -376,7 +413,34 @@ export default {
this.stompClient.heartbeat.incoming = 20000;
} catch (error) {
console.error("初始化 WebSocket 失败:", error);
this.handleDisconnect();
}
},
// 5
handleDisconnect() {
this.isWebSocketConnected = false;
this.connectionStatus = "error";
this.isReconnecting = false;
//
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(
`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`
);
this.$message.warning(
`连接断开,${this.reconnectInterval / 1000}秒后重试...`
);
setTimeout(() => {
if (!this.isWebSocketConnected) {
this.connectWebSocket();
}
}, this.reconnectInterval);
} else {
console.log("达到最大重连次数,停止重连");
this.$message.error("连接失败,请刷新页面重试");
}
},
@ -387,11 +451,19 @@ export default {
//
sendMessage() {
if (!this.inputMessage.trim() || this.connectionStatus !== "connected")
if (!this.inputMessage.trim()) return;
// WebSocket
if (!this.stompClient || !this.stompClient.connected) {
console.log('发送消息时连接已断开,尝试重连...');
this.$message.warning('连接已断开,正在重新连接...');
this.handleDisconnect();
return;
}
const messageText = this.inputMessage.trim();
try {
//
this.messages.push({
type: "user",
@ -410,7 +482,12 @@ export default {
roomId: this.roomId,
};
//
this.stompClient.send("/point/send/message", {}, JSON.stringify(message));
this.stompClient.send(
"/point/send/message/to/customer",
{},
JSON.stringify(message)
);
// WebSocket
// if (this.stompClient && this.stompClient.connected) {
// this.stompClient.send({
@ -432,6 +509,11 @@ export default {
this.$nextTick(() => {
this.scrollToBottom();
});
} catch (error) {
console.error("发送消息失败:", error);
this.$message.error("发送消息失败,请重试");
this.handleDisconnect();
}
},
// WebSocket
@ -440,7 +522,7 @@ export default {
try {
//
if (this.stompClient.subscriptions) {
Object.keys(this.stompClient.subscriptions).forEach(id => {
Object.keys(this.stompClient.subscriptions).forEach((id) => {
this.stompClient.unsubscribe(id);
});
}
@ -449,6 +531,8 @@ export default {
this.stompClient.deactivate();
this.isWebSocketConnected = false;
this.connectionStatus = "disconnected";
this.reconnectAttempts = 0;
this.isReconnecting = false;
} catch (error) {
console.error("断开 WebSocket 连接失败:", error);
}
@ -496,53 +580,61 @@ export default {
this.isLoadingHistory = true;
try {
const response = await getHistory7({ roomId: this.roomId, userType: this.userType });
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", // isSelf
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), // 使 createTime
time: new Date(msg.createTime),
id: msg.id,
roomId: msg.roomId,
sender: msg.sendEmail,
isHistory: true,
isRead: true //
isRead: true,
}));
//
this.messages = historyMessages.sort((a, b) =>
new Date(a.time) - new Date(b.time)
this.messages = historyMessages.sort(
(a, b) => new Date(a.time) - new Date(b.time)
);
//
this.$nextTick(() => {
this.scrollToBottom();
});
// DOM
await this.$nextTick();
//
setTimeout(() => {
this.scrollToBottom(true); // true
}, 100);
} else {
//
this.messages = [{
this.messages = [
{
type: "system",
text: "暂无历史消息",
isSystemHint: true,
time: new Date()
}];
time: new Date(),
},
];
}
} catch (error) {
console.error("加载历史消息失败:", error);
this.$message.error("加载历史消息失败");
//
this.messages = [{
this.messages = [
{
type: "system",
text: "加载历史消息失败,请重试",
isSystemHint: true,
time: new Date(),
isError: true
}];
isError: true,
},
];
} finally {
this.isLoadingHistory = false;
}
@ -555,6 +647,14 @@ export default {
this.isLoadingHistory = true;
try {
// ID
const oldestMessage = this.messages.find(msg => !msg.isSystemHint && !msg.isLoading);
if (!oldestMessage || !oldestMessage.id) {
console.warn('没有找到有效的消息ID');
this.hasMoreHistory = false;
return;
}
//
const loadingMsg = {
type: "system",
@ -564,8 +664,13 @@ export default {
};
this.messages.unshift(loadingMsg);
//
const response = await getHistory({ roomId: this.roomId });
// id
const response = await getHistory7({
roomId: this.roomId,
userType: this.userType,
email: this.userEmail,
id: oldestMessage.id // ID
});
//
this.messages = this.messages.filter((msg) => !msg.isLoading);
@ -630,25 +735,25 @@ export default {
roomId: msg.roomId,
sender: msg.sendEmail,
isHistory: true,
isRead: true
isRead: true,
}))
.sort((a, b) => new Date(a.time) - new Date(b.time));
},
// fetchUserid token
async fetchUserid() {
async fetchUserid(params) {
try {
// token
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 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();
const res = await getUserid(params);
if (res && res.code == 200) {
console.log("获取用户ID成功:", res);
this.receivingEmail = res.data.userEmail;
@ -699,7 +804,7 @@ export default {
id: data.id,
roomId: data.roomId,
sender: data.sendEmail,
isRead: false
isRead: false,
};
//
@ -724,7 +829,6 @@ export default {
this.$nextTick(() => {
this.scrollToBottom();
});
} catch (error) {
console.error("处理消息失败:", error);
}
@ -778,35 +882,46 @@ export default {
}
},
//
//
async toggleChat() {
this.isChatOpen = !this.isChatOpen;
// 1.
this.determineUserType();
// 2.
if (this.userType === 2) {
const lang = this.$i18n.locale;
this.$router.push(`/${lang}/customerService`);
return;
}
if (this.isChatOpen) {
//
try {
//
this.determineUserType();
// WebSocket
if (this.connectionStatus === "disconnected") {
await this.initWebSocket();
// WebSocket
if (!this.isWebSocketConnected || this.connectionStatus === "disconnected") {
await this.connectWebSocket();
}
//
if (this.messages.length === 0) {
await this.loadHistoryMessages();
} else {
//
await this.$nextTick();
setTimeout(() => {
this.scrollToBottom(true);
}, 100);
}
//
await this.markMessagesAsRead();
//
this.$nextTick(() => {
this.scrollToBottom();
});
} catch (error) {
console.error("初始化聊天失败:", error);
this.$message.error("初始化聊天失败,请重试");
}
}
},
@ -879,12 +994,13 @@ export default {
this.loadMoreHistory();
}
},
//
scrollToBottom(force = false) {
if (!this.$refs.chatBody) return;
scrollToBottom() {
if (this.$refs.chatBody) {
const scrollOptions = {
top: this.$refs.chatBody.scrollHeight,
behavior: "smooth",
behavior: force ? 'auto' : 'smooth' // 使 'auto'
};
try {
@ -893,7 +1009,6 @@ export default {
//
this.$refs.chatBody.scrollTop = this.$refs.chatBody.scrollHeight;
}
}
},
formatTime(date) {
@ -950,7 +1065,10 @@ export default {
//
async handleImageUpload(event) {
if (this.connectionStatus !== "connected") return;
if (this.connectionStatus !== "connected") {
console.log("当前连接状态:", this.connectionStatus);
return;
}
const file = event.target.files[0];
if (!file) return;
@ -981,62 +1099,71 @@ export default {
type: "info",
});
// FormData
const formData = new FormData();
formData.append("file", file);
// base64
const reader = new FileReader();
reader.onload = (e) => {
const base64Image = e.target.result;
//
const response = await getFileUpdate(formData);
console.log("文件上传返回:", response);
//
if (response && response.code === 200 && response.data) {
//
const imageData = response.data;
// 使URL
const imageUrl = this.formatImageUrl(imageData.url);
console.log("图片URL:", imageUrl); // URL
//
if (!this.stompClient || !this.stompClient.connected) {
console.error("发送图片时连接已断开");
this.$message.error("连接已断开,正在重新连接...");
this.connectWebSocket();
return;
}
//
this.messages.push({
type: "user",
text: "", //
text: "",
isImage: true,
imageUrl: imageUrl, // URL
imageUrl: base64Image,
time: new Date(),
email: this.receivingEmail,
sendUserType: this.userType,
roomId: this.roomId,
isRead: false,
});
try {
// WebSocket
if (this.stompClient && this.stompClient.connected) {
const message = {
content: imageUrl, // URL
type: 2, // 使2
content: base64Image,
type: 2,
email: this.receivingEmail,
receiveUserType: 2,
roomId: this.roomId,
};
// 使WebSocket
console.log(
"准备发送图片消息,当前连接状态:",
this.stompClient.connected
);
this.stompClient.send(
"/point/send/message",
{},
JSON.stringify(message)
);
}
console.log("图片消息发送完成");
this.$nextTick(() => {
this.scrollToBottom();
});
} else {
this.$message.error("图片上传失败: " + (response?.msg || "未知错误"));
} catch (sendError) {
console.error("发送图片消息失败:", sendError);
this.$message.error("发送图片失败,请重试");
}
};
reader.onerror = (error) => {
console.error("读取文件失败:", error);
this.$message.error("读取图片失败,请重试");
};
reader.readAsDataURL(file);
} catch (error) {
console.error("图片上传异常:", error);
this.$message.error("图片上传失败,请重试");
console.error("图片处理失败:", error);
this.$message.error("图片处理失败,请重试");
} finally {
// input
this.$refs.imageUpload.value = "";
@ -1070,7 +1197,7 @@ export default {
},
beforeDestroy() {
this.disconnectWebSocket();
//
if (this.$refs.chatBody) {
@ -1081,7 +1208,11 @@ export default {
"visibilitychange",
this.handleVisibilityChange
);
//
if (this.stompClient) {
this.stompClient.disconnect();
this.stompClient = null;
}
// WebSocket
this.disconnectWebSocket();
},

View File

@ -213,7 +213,6 @@
type="primary"
:disabled="!currentContact || !inputMessage.trim() || sending"
@click="sendMessage"
>
<i v-if="sending" class="el-icon-loading"></i>
<span v-else>发送</span>
@ -273,6 +272,7 @@ export default {
id: "", //id
roomId: "", //id
userType: 2, //
email: "497681109@qq.com", //
},
historyAllParams: {
//7
@ -282,6 +282,9 @@ export default {
},
receiveUserType: "", //
manualCreatedRooms: [], //
chatRooms: [], //
isWebSocketConnected: false,
connectionStatus: "disconnected",
};
},
computed: {
@ -307,18 +310,23 @@ export default {
},
async created() {
// WebSocket
this.initWebSocket();
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() {
//
@ -328,17 +336,14 @@ export default {
if (this.contacts.length > 0) {
this.selectContact(this.contacts[0].roomId);
}
this.confirmUserType(); //
// 30
// this.roomListInterval = setInterval(() => {
// this.fetchRoomList();
// //
// if (this.currentContactId) {
// this.loadMessages(this.currentContactId);
// }
// }, 30000);
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();
@ -365,93 +370,120 @@ export default {
//
this.sendMessage();
},
confirmUserType() {
try {
const token = localStorage.getItem("token");
if (!token) {
//
this.userType = 0;
this.userEmail = `guest_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
return;
}
try {
const userInfo = JSON.parse(
localStorage.getItem("jurisdiction") || "{}"
);
const email = JSON.parse(localStorage.getItem("userEmail") || "{}");
if (userInfo.roleKey === "customer_service") {
//
this.userType = 2;
} else {
//
this.userType = 1;
}
this.userEmail = email;
} 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)}`;
}
},
// WebSocket
initWebSocket() {
const wsUrl = `${process.env.VUE_APP_BASE_API}chat/ws`;
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: 2,
type: this.userType,
};
//
this.stompClient.connect(
headers, //
headers,
(frame) => {
//
console.log("WebSocket Connected:", frame);
this.wsConnected = true;
this.subscribeToPersonalMessages();
console.log("[客服系统] WebSocket 连接成功", frame);
this.isWebSocketConnected = true;
this.connectionStatus = "connected";
this.subscribeToMessages();
},
(error) => {
//
console.error("WebSocket Error:", error);
this.wsConnected = false;
this.$message.error("连接失败,正在重试...");
console.error("[客服系统] WebSocket 错误:", error);
this.handleDisconnect();
}
);
//
this.stompClient.heartbeat.outgoing = 20000; // 20
//
this.stompClient.heartbeat.outgoing = 20000;
this.stompClient.heartbeat.incoming = 20000;
} catch (error) {
console.error("初始化 CustomerService WebSocket 失败:", error);
this.handleDisconnect();
}
},
//
this.stompClient.debug = function (str) {
console.log("STOMP: " + str);
};
// //
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
@ -489,7 +521,7 @@ export default {
//
this.stompClient.send(
"/point/send/message",
"/point/send/message/to/user",
{},
JSON.stringify(message)
);
@ -502,7 +534,7 @@ export default {
isSelf: true,
isImage: false,
roomId: this.currentContactId,
type: 1
type: 1,
});
//
@ -530,53 +562,38 @@ export default {
},
//
handleIncomingMessage(message) {
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",
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,
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
clientReadNum: msg.clientReadNum,
};
//
if (messageData.isCreate) {
this.handleNewChatRoom(messageData);
}
this.updateChatRoomList(messageData);
//
if (messageData.roomId === this.currentContactId) {
this.addMessageToChat(messageData);
this.markMessagesAsRead(messageData.roomId);
} else {
this.incrementUnreadCount(messageData.roomId, messageData.clientReadNum);
}
},
//
handleNewChatRoom(messageData) {
//
//
const existingContact = this.contacts.find(
(c) => c.roomId === messageData.roomId
);
console.log("messageData:hhhhshshsshshshhh好的好的和", messageData);
// && messageData.isCreate && messageData.sender
if (!existingContact) {//
//
if (!existingContact) {
//
const newContact = {
roomId: messageData.roomId,
name: messageData.sender,
@ -586,8 +603,73 @@ export default {
important: false,
isGuest: messageData.sendUserType === 0,
sendUserType: messageData.sendUserType,
isManualCreated: true, //
clientReadNum: messageData.clientReadNum, //
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,
};
//
@ -616,6 +698,7 @@ export default {
this.manualCreatedRooms.push(newContact);
this.saveManualCreatedRooms();
//
this.sortContacts();
}
},
@ -657,23 +740,53 @@ export default {
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 contactIndex = this.contacts.findIndex(
(c) => c.roomId === messageData.roomId
const roomIndex = this.chatRooms.findIndex(
(room) => room.roomId === messageData.roomId
);
if (contactIndex !== -1) {
//
const contact = this.contacts[contactIndex];
contact.lastMessage = messageData.isImage
? "[图片]"
: messageData.content;
contact.lastTime = messageData.time;
if (roomIndex !== -1) {
//
this.chatRooms[roomIndex] = {
...this.chatRooms[roomIndex],
lastMessage: messageData.content,
lastMessageTime: messageData.time,
unreadCount:
messageData.clientReadNum || this.chatRooms[roomIndex].unreadCount,
};
//
this.sortContacts();
//
const updatedRoom = this.chatRooms.splice(roomIndex, 1)[0];
this.chatRooms.unshift(updatedRoom);
}
},
//
@ -721,13 +834,15 @@ export default {
this.loadingRooms = true;
const response = await getRoomList();
if (response?.code === 200) {
//
const newContacts = response.rows.map((room) => {
// roomId
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
@ -743,27 +858,19 @@ export default {
roomId: room.id,
name: room.userEmail || "未命名聊天室",
avatar: this.getDefaultAvatar(room.roomName || "未命名聊天室"),
lastMessage: room.lastMessage || "暂无消息",
// 使
lastMessage: room.lastMessage || (existingContact ? existingContact.lastMessage : "暂无消息"),
lastTime: room.lastUserSendTime
? new Date(room.lastUserSendTime)
: new Date(),
unread: existingContact?.unread ?? room.unreadCount ?? 0,
important: isImportant,
isManualCreated: existingContact?.isManualCreated || false, //
isManualCreated: manualRoom ? true : false,
sendUserType: room.sendUserType,
isGuest: room.sendUserType === 0,
};
});
//
this.manualCreatedRooms.forEach((room) => {
const exists = newContacts.find((c) => c.roomId === room.roomId);
if (!exists) {
newContacts.push({
...room,
lastTime: new Date(room.lastTime),
});
}
});
//
this.contacts = newContacts;
this.sortContacts();
@ -789,7 +896,7 @@ export default {
this.history7Params.id = lastMsg ? lastMsg.id : "";
// this.history7Params.pageNum += 1; //
this.history7Params.roomId = this.currentContactId;
this.history7Params.email = this.userEmail;
try {
this.messagesLoading = true;
@ -850,7 +957,6 @@ export default {
//
await this.markMessagesAsRead(roomId);
} catch (error) {
console.error("选择联系人失败:", error);
this.$message.error("加载聊天记录失败");
@ -878,16 +984,21 @@ export default {
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 => ({
.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",
avatar:
msg.sendUserType == 2
? "iconfont icon-icon28"
: "iconfont icon-user",
content: msg.content,
time: new Date(msg.createTime),
isSelf: msg.isSelf === 1,
@ -895,7 +1006,7 @@ export default {
isRead: msg.isRead === 1,
type: msg.type,
roomId: msg.roomId,
sendUserType: msg.sendUserType
sendUserType: msg.sendUserType,
}));
//
@ -905,11 +1016,10 @@ export default {
this.$set(this.messages, roomId, roomMessages);
//
const contact = this.contacts.find(c => c.roomId === roomId);
const contact = this.contacts.find((c) => c.roomId === roomId);
if (contact) {
contact.unread = 0;
}
} else {
//
this.$set(this.messages, roomId, []);
@ -935,7 +1045,7 @@ export default {
//
const message = {
id: messageData.id,
id: messageData.id || Date.now(), // id使
sender: messageData.sender,
avatar: messageData.avatar || (messageData.isSelf ? "iconfont icon-icon28" : "iconfont icon-user"),
content: messageData.content,
@ -944,7 +1054,7 @@ export default {
isImage: messageData.isImage || false,
type: messageData.type || 1,
roomId: roomId,
isRead: messageData.isRead || false
isRead: messageData.isRead || false,
};
//
@ -953,8 +1063,8 @@ export default {
//
this.updateContactLastMessage({
roomId: roomId,
content: message.content,
isImage: message.isImage
content: message.isImage ? "[图片]" : message.content,
isImage: message.isImage,
});
//
@ -964,10 +1074,18 @@ export default {
});
}
},
//
async handleImageUpload(event) {
if (!this.currentContact || !this.wsConnected) return;
// 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;
@ -988,63 +1106,68 @@ export default {
this.sending = true;
try {
// FormData
const formData = new FormData();
formData.append("file", file);
this.$message.info("正在上传图片...");
// API
const response = await getFileUpdate(formData);
console.log("图片上传响应:", response);
if (response && response.code === 200 && response.data) {
//
const imageData = response.data;
const imageUrl = imageData.url;
console.log("后端返回的图片URL:", imageUrl);
// base64
const reader = new FileReader();
reader.onload = (e) => {
const base64Image = e.target.result;
// WebSocket
const message = {
content: imageUrl, // 使URL
content: base64Image,
type: 2, // 2
email: this.currentContact.name,
receiveUserType: this.userType,
receiveUserType: this.currentContact.sendUserType || 1, // 使
roomId: this.currentContactId,
};
// WebSocket
this.stompClient.send(
"/point/send/message",
"/point/send/message/to/user",
{},
JSON.stringify(message)
);
// UI
//
this.addMessageToChat({
sender: "我",
avatar: "iconfont icon-icon28",
content: imageUrl,
content: base64Image,
time: new Date(),
isSelf: true,
isImage: true, //
isImage: true,
type: 2,
roomId: this.currentContactId,
});
//
this.updateContactLastMessage({
roomId: this.currentContactId,
content: "[图片]",
isImage: true
});
this.$message.success("图片已发送");
} else {
this.$message.error("图片上传失败: " + (response?.msg || "未知错误"));
}
};
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);
@ -1249,7 +1372,7 @@ export default {
}
},
// (7)
// (7)
async loadHistory() {
this.loadingHistory = true;
this.userViewHistory = true; //
@ -1258,8 +1381,17 @@ export default {
try {
this.messagesLoading = true;
//
const currentMsgs = this.messages[this.currentContactId] || [];
const response = await getHistory({ roomId: 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
@ -1320,6 +1452,9 @@ export default {
}
}
//
this.disconnectWebSocket();
// if (this.roomListInterval) {
// clearInterval(this.roomListInterval);
// }

View File

@ -76,11 +76,11 @@ export default {
nameTextStyle: {
padding: [0, 0, 0, -40],
}
},
// min: `dataMin`,
// max: `dataMax`,
// axisLabel: {
// formatter: function (value) {
axisLabel: {
formatter: function (value) {
// let data
// if (value > 10000000) {
// data = `${(value / 10000000)} KW`
@ -89,9 +89,9 @@ export default {
// } else if (value / 10000) {
// data = `${(value / 10000)} W`
// }
// return data
// }
// }
return value
}
}
},
{
@ -506,7 +506,8 @@ export default {
show: true,
amount: 1,
}
},
// { //告知已删除此币种 Radiant
// value: "dgb2_odo",
// label: "dgb-odocrypt-pool2",
@ -825,9 +826,38 @@ export default {
}
}else{ //动态计算图表的grid.left 让左侧的Y轴标签显示完全
const yAxis = this.option.yAxis[0]; // 第一个 Y 轴
const maxValue = Math.max(...this.option.series[0].data); // 获取数据最大值
const formatter = yAxis.axisLabel.formatter;
const formattedValue = formatter(maxValue); // 格式化最大值
// 创建一个临时 DOM 元素计算宽度
const tempDiv = document.createElement('div');
tempDiv.style.position = 'absolute';
tempDiv.style.visibility = 'hidden';
tempDiv.style.fontSize = '12px'; // 与 axisLabel.fontSize 一致
tempDiv.innerText = formattedValue;
document.body.appendChild(tempDiv);
const labelWidth = tempDiv.offsetWidth;
document.body.removeChild(tempDiv);
// 动态设置 grid.left加上安全边距
const safeMargin = 20;
this.option.grid.left = labelWidth + safeMargin + 'px';
// this.$nextTick(
// // 更新图表
// this.inCharts()
// )
}
this.getBlockInfoData(this.BlockInfoParams)
this.getCoinInfoData(this.params)
this.getPoolPowerData(this.PowerParams)
@ -1395,52 +1425,111 @@ export default {
handelCalculation() {
this.calculateIncome()
},
// 获取单个币种项目的完整宽度包含margin
getItemFullWidth() {
const listEl = this.$refs.currencyList;
if (!listEl) return 120;
const firstItem = listEl.querySelector('.list-item');
if (!firstItem) return 120;
const style = window.getComputedStyle(firstItem);
const width = firstItem.offsetWidth;
const marginLeft = parseInt(style.marginLeft) || 0;
const marginRight = parseInt(style.marginRight) || 0;
return width + marginLeft + marginRight;
},
// 左滑动逻辑
scrollLeft() {
const allLength = this.currencyList.length * 120
const boxLength = document.getElementById('list-box').clientWidth
if (allLength < boxLength) return
const listEl = document.getElementById('list')
const leftMove = Math.abs(parseInt(window.getComputedStyle(listEl, null)?.left))
if (leftMove + boxLength - 360 < boxLength) {
// 到最开始的时候
listEl.style.left = '0PX'
const listEl = this.$refs.currencyList;
const listBox = this.$refs.listBox;
if (!listEl || !listBox) return;
const itemFullWidth = this.getItemFullWidth();
const step = 2 * itemFullWidth; // 每次滑动2个币种
const allLength = this.currencyList.length * itemFullWidth;
const boxLength = listBox.clientWidth;
if (allLength <= boxLength) return;
let currentLeft = Math.abs(parseInt(listEl.style.transform.replace('translateX(', '').replace('px)', '')) || 0);
let newLeft = currentLeft - step;
listEl.classList.add('scrolling');
if (newLeft <= 0) {
listEl.style.transform = 'translateX(0)';
} else {
listEl.style.left = '-' + (leftMove - 360) + 'PX'
}
},
// 右滑动逻辑
scrollRight() {
const allLength = this.currencyList.length * 120
const boxLength = document.getElementById('list-box').clientWidth
if (allLength < boxLength) return
const listEl = document.getElementById('list')
const leftMove = Math.abs(parseInt(window.getComputedStyle(listEl, null)?.left))
if (leftMove + boxLength + 360 > allLength) {
listEl.style.left = '-' + (allLength - boxLength) + 'PX'
} else {
listEl.style.left = '-' + (leftMove + 360) + 'PX'
listEl.style.transform = `translateX(-${newLeft}px)`;
}
// 增加动画时间到 500ms
setTimeout(() => {
listEl.classList.remove('scrolling');
}, 500);
},
// clickCurrency: throttle(function(item) {
// this.currency = item.label
// this.currencyPath = item.imgUrl
// this.params.coin = item.value
// this.BlockInfoParams.coin = item.value
// this.itemActive = item.value
// this.PowerParams.coin = item.value
// this.getCoinInfoData(this.params)
// this.getBlockInfoData(this.BlockInfoParams)
// // this.getPoolPowerData(this.PowerParams)
// // this.getMinerCountData(this.params)
// if (this.powerActive) {
// this.handelPower()
// } else if (!this.powerActive) {
// this.handelMiner()
// 右滑动逻辑
scrollRight() {
const listEl = this.$refs.currencyList;
const listBox = this.$refs.listBox;
if (!listEl || !listBox) return;
const itemFullWidth = this.getItemFullWidth();
const step = 2 * itemFullWidth; // 每次滑动2个币种
const allLength = this.currencyList.length * itemFullWidth;
const boxLength = listBox.clientWidth;
if (allLength <= boxLength) return;
let currentLeft = Math.abs(parseInt(listEl.style.transform.replace('translateX(', '').replace('px)', '')) || 0);
let newLeft = currentLeft + step;
const maxLeft = allLength - boxLength;
listEl.classList.add('scrolling');
if (newLeft >= maxLeft) {
listEl.style.transform = `translateX(-${maxLeft}px)`;
} else {
listEl.style.transform = `translateX(-${newLeft}px)`;
}
// 增加动画时间到 500ms
setTimeout(() => {
listEl.classList.remove('scrolling');
}, 500);
},
// // 左滑动逻辑
// scrollLeft() {
// const allLength = this.currencyList.length * 120
// const boxLength = document.getElementById('list-box').clientWidth
// if (allLength < boxLength) return
// const listEl = document.getElementById('list')
// const leftMove = Math.abs(parseInt(window.getComputedStyle(listEl, null)?.left))
// if (leftMove + boxLength - 360 < boxLength) {
// // 到最开始的时候
// listEl.style.left = '0PX'
// } else {
// listEl.style.left = '-' + (leftMove - 360) + 'PX'
// }
// }, 1000),
// },
// // 右滑动逻辑
// scrollRight() {
// const allLength = this.currencyList.length * 120
// const boxLength = document.getElementById('list-box').clientWidth
// if (allLength < boxLength) return
// const listEl = document.getElementById('list')
// const leftMove = Math.abs(parseInt(window.getComputedStyle(listEl, null)?.left))
// if (leftMove + boxLength + 360 > allLength) {
// listEl.style.left = '-' + (allLength - boxLength) + 'PX'
// } else {
// listEl.style.left = '-' + (leftMove + 360) + 'PX'
// }
// },
handleActiveItemChange(item) {
if (!item) return;

View File

@ -340,6 +340,29 @@
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-card>
<div class="monitor-list">
<div class="btn left" @click="scrollLeft">
<i class="iconfont icon-icon-prev" />
</div>
<div ref="listBox" class="list-box">
<div ref="currencyList" class="list">
<div
v-for="item in currencyList"
:key="item.value"
@click="clickCurrency(item)"
class="list-item"
>
<img :src="item.img" alt="coin" />
<span :class="{ active: itemActive === item.value }">
{{ item.label }}
</span>
</div>
</div>
</div>
<div class="btn right" @click="scrollRight">
<i class="iconfont icon-zuoyoujiantou1" />
</div>
</div>
<!-- <div class="monitor-list">
<div class="btn left" @click="scrollLeft">
<i class="iconfont icon-icon-prev" />
</div>
@ -361,7 +384,7 @@
<div class="btn right" @click="scrollRight">
<i class="iconfont icon-zuoyoujiantou1" />
</div>
</div>
</div> -->
</el-card>
</el-col>
</el-row>
@ -406,7 +429,6 @@
>
</div>
<div
id="chart"
v-if="powerActive"
@ -3119,6 +3141,63 @@ export default {
transition: left 1s;
}
}
.list-box {//
width: calc(100% - 100px);
overflow: hidden;
position: relative;
}
.list {
display: flex;
will-change: transform;
padding-left: 2%;
&.scrolling {
// 0.5s使
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.list-item {
flex-shrink: 0;
width: 120px;
height: 95%;
margin-left: 18px;
// 使
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
&:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(110, 62, 219, 0.2);
}
img {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
span {
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
}
}
.btn {
//
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
&:hover {
background-color: #6e3edb;
color: #fff;
transform: scale(1.05);
}
&:active {
transform: scale(0.95);
}
}
}
// -----------------------