feat(wechat): 实现微信聊天状态管理及消息处理功能
添加微信聊天状态管理store,包含联系人管理、消息发送/接收、未读消息计数等功能 修复聊天窗口参数传递问题,统一使用contract.id作为标识 调整消息加载数量从10条减少到5条
This commit is contained in:
@@ -38,7 +38,7 @@ const MessageEnter: React.FC<MessageEnterProps> = ({
|
||||
console.log("发送消息", contract);
|
||||
const params = {
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatChatroomId: contract?.chatroomId || 0,
|
||||
wechatChatroomId: contract?.chatroomId ? contract.id : 0,
|
||||
wechatFriendId: contract?.chatroomId ? 0 : contract.id,
|
||||
msgSubType: 0,
|
||||
msgType: 1,
|
||||
|
||||
@@ -64,11 +64,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
From: 1,
|
||||
To: 4704624000000,
|
||||
Count: 10,
|
||||
Count: 5,
|
||||
olderData: true,
|
||||
};
|
||||
if (contract.chatroomId) {
|
||||
params.wechatChatroomId = contract.chatroomId;
|
||||
params.wechatChatroomId = contract.id;
|
||||
} else {
|
||||
params.wechatFriendId = contract.id;
|
||||
}
|
||||
|
||||
601
Cunkebao/src/store/module/weChat.ts
Normal file
601
Cunkebao/src/store/module/weChat.ts
Normal file
@@ -0,0 +1,601 @@
|
||||
import { create } from "zustand";
|
||||
import { createPersistStore } from "../createPersistStore";
|
||||
import { getChatMessages } from "@/pages/pc/ckbox/api";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { useWebSocketStore } from "./websocket/websocket";
|
||||
|
||||
// 聊天列表项类型定义
|
||||
export interface ChatListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
type: "friend" | "group";
|
||||
lastMessage?: ChatRecord;
|
||||
unreadCount: number;
|
||||
isOnline?: boolean;
|
||||
lastActiveTime: number; // 最后活跃时间,用于排序
|
||||
contractData: ContractData | weChatGroup; // 保存完整的联系人数据
|
||||
}
|
||||
|
||||
// 微信聊天相关的类型定义
|
||||
export interface WeChatState {
|
||||
// 当前选中的联系人/群组
|
||||
currentContract: ContractData | weChatGroup | null;
|
||||
|
||||
// 当前聊天用户的消息列表(只存储当前聊天用户的消息)
|
||||
currentMessages: ChatRecord[];
|
||||
|
||||
// 联系人列表(所有好友和群组)
|
||||
contacts: (ContractData | weChatGroup)[];
|
||||
|
||||
// 聊天列表(有聊天记录的联系人/群组,按最后活跃时间排序)
|
||||
chatList: ChatListItem[];
|
||||
|
||||
// 消息发送状态
|
||||
sendingStatus: Record<string, boolean>; // key为消息临时ID,value为发送状态
|
||||
|
||||
// 消息加载状态
|
||||
loadingMessages: boolean;
|
||||
|
||||
// 待处理的视频请求
|
||||
pendingVideoRequests: Record<string, string>; // messageId -> requestId
|
||||
|
||||
// 输入框内容状态 - 按联系人/群组ID分组存储
|
||||
inputValues: Record<string, string>;
|
||||
|
||||
// 素材模态框状态
|
||||
showMaterialModal: boolean;
|
||||
selectedMaterialType: string;
|
||||
}
|
||||
|
||||
export interface WeChatActions {
|
||||
// 设置当前选中的联系人/群组
|
||||
setCurrentContract: (contract: ContractData | weChatGroup | null) => void;
|
||||
|
||||
// 获取聊天消息
|
||||
fetchChatMessages: (contract: ContractData | weChatGroup) => Promise<void>;
|
||||
|
||||
// 设置当前消息列表
|
||||
setCurrentMessages: (messages: ChatRecord[]) => void;
|
||||
|
||||
// 添加消息到当前聊天
|
||||
addMessageToCurrentChat: (message: ChatRecord) => void;
|
||||
|
||||
// 更新消息内容(用于视频加载状态更新等)
|
||||
updateMessage: (messageId: number, updates: Partial<ChatRecord>) => void;
|
||||
|
||||
// 发送消息
|
||||
sendMessage: (
|
||||
contract: ContractData | weChatGroup,
|
||||
content: string,
|
||||
msgType?: number,
|
||||
) => Promise<void>;
|
||||
|
||||
// 处理视频播放请求
|
||||
handleVideoPlayRequest: (
|
||||
tencentUrl: string,
|
||||
messageId: number,
|
||||
contract: ContractData | weChatGroup,
|
||||
) => void;
|
||||
|
||||
// 设置待处理的视频请求
|
||||
setPendingVideoRequest: (messageId: string, requestId: string) => void;
|
||||
|
||||
// 移除待处理的视频请求
|
||||
removePendingVideoRequest: (messageId: string) => void;
|
||||
|
||||
// 处理视频下载响应
|
||||
handleVideoDownloadResponse: (messageId: string, videoUrl: string) => void;
|
||||
|
||||
// 设置输入框内容
|
||||
setInputValue: (contractId: string, value: string) => void;
|
||||
|
||||
// 获取输入框内容
|
||||
getInputValue: (contractId: string) => string;
|
||||
|
||||
// 清空输入框内容
|
||||
clearInputValue: (contractId: string) => void;
|
||||
|
||||
// 素材模态框相关操作
|
||||
setShowMaterialModal: (show: boolean) => void;
|
||||
setSelectedMaterialType: (type: string) => void;
|
||||
|
||||
// 新消息处理
|
||||
handleNewMessage: (
|
||||
message: ChatRecord,
|
||||
fromContract: ContractData | weChatGroup,
|
||||
) => void;
|
||||
|
||||
// 聊天列表管理
|
||||
setContacts: (contacts: (ContractData | weChatGroup)[]) => void;
|
||||
setChatList: (chatList: ChatListItem[]) => void;
|
||||
updateChatListItem: (
|
||||
contractId: string,
|
||||
updates: Partial<ChatListItem>,
|
||||
) => void;
|
||||
moveToTopOfChatList: (contractId: string) => void;
|
||||
addToChatList: (contract: ContractData | weChatGroup) => void;
|
||||
|
||||
// 未读消息管理
|
||||
incrementUnreadCount: (contractId: string) => void;
|
||||
clearUnreadCount: (contractId: string) => void;
|
||||
|
||||
// 便捷选择器
|
||||
getUnreadTotal: () => number;
|
||||
getChatListSorted: () => ChatListItem[];
|
||||
|
||||
// 清理指定联系人的数据
|
||||
clearContractData: (contractId: string) => void;
|
||||
|
||||
// 清理所有数据
|
||||
clearAllData: () => void;
|
||||
}
|
||||
|
||||
type WeChatStore = WeChatState & WeChatActions;
|
||||
|
||||
// 生成联系人/群组的唯一ID
|
||||
const getContractId = (contract: ContractData | weChatGroup): string => {
|
||||
if ("chatroomId" in contract && contract.chatroomId) {
|
||||
return `group_${contract.chatroomId}`;
|
||||
}
|
||||
return `friend_${contract.id}`;
|
||||
};
|
||||
|
||||
export const useWeChatStore = create<WeChatStore>()(
|
||||
createPersistStore(
|
||||
(set, get) => ({
|
||||
// 初始状态
|
||||
currentContract: null,
|
||||
currentMessages: [],
|
||||
contacts: [],
|
||||
chatList: [],
|
||||
sendingStatus: {},
|
||||
loadingMessages: false,
|
||||
pendingVideoRequests: {},
|
||||
inputValues: {},
|
||||
showMaterialModal: false,
|
||||
selectedMaterialType: "",
|
||||
|
||||
// Actions
|
||||
setCurrentContract: contract => {
|
||||
set({ currentContract: contract });
|
||||
// 切换联系人时清空当前消息,等待重新加载
|
||||
set({ currentMessages: [] });
|
||||
// 清除该联系人的未读数
|
||||
if (contract) {
|
||||
const contractId = getContractId(contract);
|
||||
get().clearUnreadCount(contractId);
|
||||
}
|
||||
},
|
||||
|
||||
fetchChatMessages: async contract => {
|
||||
const { currentMessages } = get();
|
||||
|
||||
// 如果已经有消息数据,不重复加载
|
||||
if (currentMessages.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
set({ loadingMessages: true });
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
From: 1,
|
||||
To: 4704624000000,
|
||||
Count: 10,
|
||||
olderData: true,
|
||||
};
|
||||
|
||||
if ("chatroomId" in contract && contract.chatroomId) {
|
||||
params.wechatChatroomId = contract.chatroomId;
|
||||
} else {
|
||||
params.wechatFriendId = contract.id;
|
||||
}
|
||||
|
||||
const messages = await getChatMessages(params);
|
||||
|
||||
set({ currentMessages: messages });
|
||||
} catch (error) {
|
||||
console.error("获取聊天消息失败:", error);
|
||||
} finally {
|
||||
set({ loadingMessages: false });
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentMessages: messages => {
|
||||
set({ currentMessages: messages });
|
||||
},
|
||||
|
||||
addMessageToCurrentChat: message => {
|
||||
set(state => ({
|
||||
currentMessages: [...state.currentMessages, message],
|
||||
}));
|
||||
},
|
||||
|
||||
updateMessage: (messageId, updates) => {
|
||||
set(state => ({
|
||||
currentMessages: state.currentMessages.map(msg =>
|
||||
msg.id === messageId ? { ...msg, ...updates } : msg,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
sendMessage: async (contract, content, msgType = 1) => {
|
||||
const contractId = getContractId(contract);
|
||||
const tempId = `temp_${Date.now()}`;
|
||||
|
||||
// 设置发送状态
|
||||
set(state => ({
|
||||
sendingStatus: {
|
||||
...state.sendingStatus,
|
||||
[tempId]: true,
|
||||
},
|
||||
}));
|
||||
|
||||
try {
|
||||
const params = {
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
wechatChatroomId:
|
||||
"chatroomId" in contract && contract.chatroomId
|
||||
? contract.chatroomId
|
||||
: 0,
|
||||
wechatFriendId:
|
||||
"chatroomId" in contract && contract.chatroomId ? 0 : contract.id,
|
||||
msgSubType: 0,
|
||||
msgType,
|
||||
content,
|
||||
};
|
||||
|
||||
// 通过WebSocket发送消息
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
await sendCommand("CmdSendMessage", params);
|
||||
|
||||
// 清空对应的输入框内容
|
||||
get().clearInputValue(contractId);
|
||||
} catch (error) {
|
||||
console.error("发送消息失败:", error);
|
||||
} finally {
|
||||
// 移除发送状态
|
||||
set(state => {
|
||||
const newSendingStatus = { ...state.sendingStatus };
|
||||
delete newSendingStatus[tempId];
|
||||
return { sendingStatus: newSendingStatus };
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleVideoPlayRequest: (tencentUrl, messageId, contract) => {
|
||||
const requestSeq = `${Date.now()}`;
|
||||
|
||||
console.log("发送视频下载请求:", { messageId, requestSeq });
|
||||
|
||||
// 构建socket请求数据
|
||||
const { sendCommand } = useWebSocketStore.getState();
|
||||
sendCommand("CmdDownloadVideo", {
|
||||
chatroomMessageId:
|
||||
"chatroomId" in contract && contract.chatroomId ? messageId : 0,
|
||||
friendMessageId:
|
||||
"chatroomId" in contract && contract.chatroomId ? 0 : messageId,
|
||||
seq: requestSeq,
|
||||
tencentUrl: tencentUrl,
|
||||
wechatAccountId: contract.wechatAccountId,
|
||||
});
|
||||
|
||||
// 添加到待处理队列
|
||||
get().setPendingVideoRequest(
|
||||
messageId.toString(),
|
||||
messageId.toString(),
|
||||
);
|
||||
|
||||
// 更新消息状态为加载中
|
||||
const messages = get().currentMessages;
|
||||
const targetMessage = messages.find(msg => msg.id === messageId);
|
||||
if (targetMessage) {
|
||||
try {
|
||||
const originalContent =
|
||||
typeof targetMessage.content === "string"
|
||||
? JSON.parse(targetMessage.content)
|
||||
: targetMessage.content;
|
||||
|
||||
get().updateMessage(messageId, {
|
||||
content: JSON.stringify({
|
||||
...originalContent,
|
||||
isLoading: true,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("解析消息内容失败:", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setPendingVideoRequest: (messageId, requestId) => {
|
||||
set(state => ({
|
||||
pendingVideoRequests: {
|
||||
...state.pendingVideoRequests,
|
||||
[messageId]: requestId,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
removePendingVideoRequest: messageId => {
|
||||
set(state => {
|
||||
const newRequests = { ...state.pendingVideoRequests };
|
||||
delete newRequests[messageId];
|
||||
return { pendingVideoRequests: newRequests };
|
||||
});
|
||||
},
|
||||
|
||||
handleVideoDownloadResponse: (messageId, videoUrl) => {
|
||||
const { currentMessages } = get();
|
||||
const targetMessage = currentMessages.find(
|
||||
msg => msg.id === Number(messageId),
|
||||
);
|
||||
|
||||
if (targetMessage) {
|
||||
try {
|
||||
const msgContent =
|
||||
typeof targetMessage.content === "string"
|
||||
? JSON.parse(targetMessage.content)
|
||||
: targetMessage.content;
|
||||
|
||||
// 更新消息内容,添加视频URL并移除加载状态
|
||||
get().updateMessage(Number(messageId), {
|
||||
content: JSON.stringify({
|
||||
...msgContent,
|
||||
videoUrl: videoUrl,
|
||||
isLoading: false,
|
||||
}),
|
||||
});
|
||||
|
||||
// 从待处理队列中移除
|
||||
get().removePendingVideoRequest(messageId);
|
||||
} catch (e) {
|
||||
console.error("解析消息内容失败:", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
setInputValue: (contractId, value) => {
|
||||
set(state => ({
|
||||
inputValues: {
|
||||
...state.inputValues,
|
||||
[contractId]: value,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
getInputValue: contractId => {
|
||||
return get().inputValues[contractId] || "";
|
||||
},
|
||||
|
||||
clearInputValue: contractId => {
|
||||
set(state => {
|
||||
const newInputValues = { ...state.inputValues };
|
||||
delete newInputValues[contractId];
|
||||
return { inputValues: newInputValues };
|
||||
});
|
||||
},
|
||||
|
||||
setShowMaterialModal: show => {
|
||||
set({ showMaterialModal: show });
|
||||
},
|
||||
|
||||
setSelectedMaterialType: type => {
|
||||
set({ selectedMaterialType: type });
|
||||
},
|
||||
|
||||
// 新消息处理
|
||||
handleNewMessage: (message, fromContract) => {
|
||||
const { currentContract } = get();
|
||||
const contractId = getContractId(fromContract);
|
||||
|
||||
// 如果是当前聊天用户的消息,直接添加到当前消息列表
|
||||
if (currentContract && getContractId(currentContract) === contractId) {
|
||||
get().addMessageToCurrentChat(message);
|
||||
} else {
|
||||
// 如果不是当前聊天用户,更新聊天列表
|
||||
get().incrementUnreadCount(contractId);
|
||||
get().moveToTopOfChatList(contractId);
|
||||
|
||||
// 更新聊天列表项的最后消息
|
||||
get().updateChatListItem(contractId, {
|
||||
lastMessage: message,
|
||||
lastActiveTime: message.createTime || Date.now(),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 聊天列表管理
|
||||
setContacts: contacts => {
|
||||
set({ contacts });
|
||||
},
|
||||
|
||||
setChatList: chatList => {
|
||||
set({ chatList });
|
||||
},
|
||||
|
||||
updateChatListItem: (contractId, updates) => {
|
||||
set(state => ({
|
||||
chatList: state.chatList.map(item =>
|
||||
getContractId(item.contractData) === contractId
|
||||
? { ...item, ...updates }
|
||||
: item,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
moveToTopOfChatList: contractId => {
|
||||
set(state => {
|
||||
const chatList = [...state.chatList];
|
||||
const itemIndex = chatList.findIndex(
|
||||
item => getContractId(item.contractData) === contractId,
|
||||
);
|
||||
|
||||
if (itemIndex > 0) {
|
||||
// 移动到顶部
|
||||
const item = chatList.splice(itemIndex, 1)[0];
|
||||
chatList.unshift({ ...item, lastActiveTime: Date.now() });
|
||||
} else if (itemIndex === -1) {
|
||||
// 如果不在聊天列表中,从联系人列表找到并添加
|
||||
const contract = state.contacts.find(
|
||||
c => getContractId(c) === contractId,
|
||||
);
|
||||
if (contract) {
|
||||
get().addToChatList(contract);
|
||||
}
|
||||
}
|
||||
|
||||
return { chatList };
|
||||
});
|
||||
},
|
||||
|
||||
addToChatList: contract => {
|
||||
set(state => {
|
||||
const contractId = getContractId(contract);
|
||||
// 检查是否已存在
|
||||
const exists = state.chatList.some(
|
||||
item => getContractId(item.contractData) === contractId,
|
||||
);
|
||||
if (exists) return state;
|
||||
|
||||
const newChatItem: ChatListItem = {
|
||||
id: contractId,
|
||||
name: contract.nickName || contract.name || "",
|
||||
avatar: contract.avatar,
|
||||
type: "chatroomId" in contract ? "group" : "friend",
|
||||
unreadCount: 0,
|
||||
lastActiveTime: Date.now(),
|
||||
contractData: contract,
|
||||
};
|
||||
|
||||
return {
|
||||
chatList: [newChatItem, ...state.chatList],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// 未读消息管理
|
||||
incrementUnreadCount: contractId => {
|
||||
// 更新聊天列表中的未读数
|
||||
set(state => ({
|
||||
chatList: state.chatList.map(item =>
|
||||
getContractId(item.contractData) === contractId
|
||||
? { ...item, unreadCount: item.unreadCount + 1 }
|
||||
: item,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
clearUnreadCount: contractId => {
|
||||
// 清除聊天列表中的未读数
|
||||
set(state => ({
|
||||
chatList: state.chatList.map(item =>
|
||||
getContractId(item.contractData) === contractId
|
||||
? { ...item, unreadCount: 0 }
|
||||
: item,
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
// 便捷选择器
|
||||
getUnreadTotal: () => {
|
||||
const { chatList } = get();
|
||||
return chatList.reduce((total, item) => total + item.unreadCount, 0);
|
||||
},
|
||||
|
||||
getChatListSorted: () => {
|
||||
const { chatList } = get();
|
||||
return [...chatList].sort(
|
||||
(a, b) => b.lastActiveTime - a.lastActiveTime,
|
||||
);
|
||||
},
|
||||
|
||||
clearContractData: contractId => {
|
||||
set(state => {
|
||||
const newInputValues = { ...state.inputValues };
|
||||
delete newInputValues[contractId];
|
||||
|
||||
return {
|
||||
inputValues: newInputValues,
|
||||
chatList: state.chatList.filter(
|
||||
item => getContractId(item.contractData) !== contractId,
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
clearAllData: () => {
|
||||
set({
|
||||
currentContract: null,
|
||||
currentMessages: [],
|
||||
contacts: [],
|
||||
chatList: [],
|
||||
sendingStatus: {},
|
||||
loadingMessages: false,
|
||||
pendingVideoRequests: {},
|
||||
inputValues: {},
|
||||
showMaterialModal: false,
|
||||
selectedMaterialType: "",
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "wechat-store",
|
||||
// 只持久化部分状态,排除临时状态
|
||||
partialize: state => ({
|
||||
contacts: state.contacts,
|
||||
chatList: state.chatList,
|
||||
inputValues: state.inputValues,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 导出便捷的选择器函数
|
||||
export const useCurrentContract = () =>
|
||||
useWeChatStore(state => state.currentContract);
|
||||
export const useCurrentMessages = () =>
|
||||
useWeChatStore(state => state.currentMessages);
|
||||
export const useCurrentInputValue = () => {
|
||||
const { currentContract, getInputValue } = useWeChatStore();
|
||||
if (!currentContract) return "";
|
||||
const contractId = getContractId(currentContract);
|
||||
return getInputValue(contractId);
|
||||
};
|
||||
export const useChatList = () =>
|
||||
useWeChatStore(state => state.getChatListSorted());
|
||||
export const useUnreadTotal = () =>
|
||||
useWeChatStore(state => state.getUnreadTotal());
|
||||
|
||||
// 初始化WebSocket消息监听
|
||||
if (typeof window !== "undefined") {
|
||||
// 监听WebSocket消息,处理视频下载响应
|
||||
useWebSocketStore.subscribe(state => {
|
||||
const messages = state.messages as any[];
|
||||
const { pendingVideoRequests, handleVideoDownloadResponse } =
|
||||
useWeChatStore.getState();
|
||||
|
||||
// 只有当有待处理的视频请求时才处理消息
|
||||
if (Object.keys(pendingVideoRequests).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
messages.forEach(message => {
|
||||
if (message?.content?.cmdType === "CmdDownloadVideoResult") {
|
||||
console.log("收到视频下载响应:", message.content);
|
||||
|
||||
// 检查是否是我们正在等待的视频响应
|
||||
const messageId = Object.keys(pendingVideoRequests).find(
|
||||
id => pendingVideoRequests[id] === message.content.friendMessageId,
|
||||
);
|
||||
|
||||
if (messageId) {
|
||||
console.log("找到对应的消息ID:", messageId);
|
||||
handleVideoDownloadResponse(messageId, message.content.url);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { deepCopy } from "@/utils/common";
|
||||
import { WebSocketMessage } from "./websocket";
|
||||
import { getkfUserList, asyncKfUserList } from "@/store/module/ckchat/ckchat";
|
||||
import { Messages } from "./msg.data";
|
||||
// 消息处理器类型定义
|
||||
type MessageHandler = (message: WebSocketMessage) => void;
|
||||
|
||||
@@ -30,7 +31,7 @@ const messageHandlers: Record<string, MessageHandler> = {
|
||||
// 在这里添加具体的处理逻辑
|
||||
},
|
||||
//收到消息
|
||||
CmdNewMessage: message => {
|
||||
CmdNewMessage: (message: Messages) => {
|
||||
console.log("收到消息", message.friendMessage);
|
||||
// 在这里添加具体的处理逻辑
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user