diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx
index e057a728..b2838f7e 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx
@@ -1,15 +1,5 @@
-import React, { useEffect, useState, useRef } from "react";
-import {
- Layout,
- Input,
- Button,
- Modal,
- message,
- Tooltip,
- AutoComplete,
- Input as AntInput,
- Spin,
-} from "antd";
+import React, { useEffect, useState, useCallback } from "react";
+import { Layout, Button, message, Tooltip } from "antd";
import {
SendOutlined,
FolderOutlined,
@@ -36,7 +26,6 @@ import {
import { useContactStore } from "@/store/module/weChat/contacts";
import SelectMap from "./components/selectMap";
const { Footer } = Layout;
-const { TextArea } = Input;
interface MessageEnterProps {
contract: ContractData | weChatGroup;
@@ -44,6 +33,159 @@ interface MessageEnterProps {
const { sendCommand } = useWebSocketStore.getState();
+const FileType = {
+ TEXT: 1,
+ IMAGE: 2,
+ VIDEO: 3,
+ AUDIO: 4,
+ FILE: 5,
+};
+
+const IMAGE_FORMATS = [
+ "jpg",
+ "jpeg",
+ "png",
+ "gif",
+ "bmp",
+ "webp",
+ "svg",
+ "ico",
+];
+
+const VIDEO_FORMATS = [
+ "mp4",
+ "avi",
+ "mov",
+ "wmv",
+ "flv",
+ "mkv",
+ "webm",
+ "3gp",
+ "rmvb",
+];
+
+// 根据文件格式判断消息类型(纯函数,放在组件外避免重复创建)
+const getMsgTypeByFileFormat = (filePath: string): number => {
+ const extension = filePath.toLowerCase().split(".").pop() || "";
+
+ if (IMAGE_FORMATS.includes(extension)) {
+ return 3; // 图片
+ }
+
+ if (VIDEO_FORMATS.includes(extension)) {
+ return 43; // 视频
+ }
+
+ // 其他格式默认为文件
+ return 49; // 文件
+};
+
+const InputToolbar = React.memo(
+ ({
+ isAiAssist,
+ isAiTakeover,
+ isLoadingAiChat,
+ onEmojiSelect,
+ onFileUploaded,
+ onAudioUploaded,
+ onOpenMap,
+ onManualTriggerAi,
+ onOpenChatRecord,
+ }: {
+ isAiAssist: boolean;
+ isAiTakeover: boolean;
+ isLoadingAiChat: boolean;
+ onEmojiSelect: (emoji: EmojiInfo) => void;
+ onFileUploaded: (
+ filePath: { url: string; name: string; durationMs?: number },
+ fileType: number,
+ ) => void;
+ onAudioUploaded: (audioData: {
+ name: string;
+ url: string;
+ durationMs?: number;
+ }) => void;
+ onOpenMap: () => void;
+ onManualTriggerAi: () => void;
+ onOpenChatRecord: () => void;
+ }) => {
+ return (
+
+
+
+
onFileUploaded(fileInfo, FileType.FILE)}
+ maxSize={10}
+ type={4}
+ slot={
+ }
+ />
+ }
+ />
+
+ onFileUploaded(fileInfo, FileType.IMAGE)
+ }
+ maxSize={10}
+ type={1}
+ slot={
+ }
+ />
+ }
+ />
+
+
+ }
+ onClick={onOpenMap}
+ />
+
+ {/* AI模式下显示重新生成按钮 */}
+ {(isAiAssist || isAiTakeover) && (
+
+ }
+ onClick={onManualTriggerAi}
+ disabled={isLoadingAiChat}
+ />
+
+ )}
+
+
+
+ );
+ },
+);
+
+const MemoSelectMap: React.FC> =
+ React.memo(props => );
+
const MessageEnter: React.FC = ({ contract }) => {
const [inputValue, setInputValue] = useState("");
const EnterModule = useWeChatStore(state => state.EnterModule);
@@ -78,270 +220,267 @@ const MessageEnter: React.FC = ({ contract }) => {
const isAiTakeover = aiQuoteMessageContent === 2; // AI接管
// 取消AI生成
- const handleCancelAi = () => {
- // 清除AI请求定时器和队列
+ const handleCancelAi = useCallback(() => {
clearAiRequestQueue("用户手动取消");
- // 停止AI加载状态
updateIsLoadingAiChat(false);
- // 清空AI回复内容
updateQuoteMessageContent("");
message.info("已取消AI生成");
- };
+ }, [updateIsLoadingAiChat, updateQuoteMessageContent]);
// 监听输入框变化 - 用户开始输入时取消AI
- const handleInputChange = (e: React.ChangeEvent) => {
- const newValue = e.target.value;
- setInputValue(newValue);
+ const handleInputChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const newValue = e.target.value;
+ setInputValue(newValue);
- // 如果用户开始输入(且不是AI填充的内容),取消AI请求
- if (newValue && newValue !== quoteMessageContent) {
- if (isLoadingAiChat) {
+ // 如果用户开始输入(且不是AI填充的内容),取消AI请求
+ if (newValue && newValue !== quoteMessageContent && isLoadingAiChat) {
console.log("👤 用户开始输入,取消AI生成");
clearAiRequestQueue("用户开始输入");
updateIsLoadingAiChat(false);
updateQuoteMessageContent("");
}
- }
- };
+ },
+ [
+ isLoadingAiChat,
+ quoteMessageContent,
+ updateIsLoadingAiChat,
+ updateQuoteMessageContent,
+ ],
+ );
// 手动触发AI生成
- const handleManualTriggerAi = async () => {
+ const handleManualTriggerAi = useCallback(async () => {
const success = await manualTriggerAi();
if (success) {
message.success("AI正在生成回复...");
} else {
message.warning("无法生成AI回复,请检查消息记录");
}
- };
+ }, []);
// 发送消息(支持传入内容参数,避免闭包问题)
- const handleSend = async (content?: string) => {
- const messageContent = content || inputValue; // 优先使用传入的内容
+ const handleSend = useCallback(
+ async (content?: string) => {
+ const messageContent = content || inputValue; // 优先使用传入的内容
- if (!messageContent || !messageContent.trim()) {
- console.warn("消息内容为空,取消发送");
- return;
- }
+ if (!messageContent || !messageContent.trim()) {
+ console.warn("消息内容为空,取消发送");
+ return;
+ }
- // 用户主动发送消息时,取消AI请求
- if (!content && isLoadingAiChat) {
- console.log("👤 用户主动发送消息,取消AI生成");
- clearAiRequestQueue("用户主动发送");
- updateIsLoadingAiChat(false);
- }
+ // 用户主动发送消息时,取消AI请求
+ if (!content && isLoadingAiChat) {
+ console.log("👤 用户主动发送消息,取消AI生成");
+ clearAiRequestQueue("用户主动发送");
+ updateIsLoadingAiChat(false);
+ }
- console.log("handleSend", messageContent);
- const messageId = +Date.now();
- // 构造本地消息对象
- const localMessage: ChatRecord = {
- id: messageId, // 使用时间戳作为临时ID
- wechatAccountId: contract.wechatAccountId,
- wechatFriendId: contract?.chatroomId ? 0 : contract.id,
- wechatChatroomId: contract?.chatroomId ? contract.id : 0,
- tenantId: 0,
- accountId: 0,
- synergyAccountId: 0,
- content: messageContent,
- msgType: 1,
- msgSubType: 0,
- msgSvrId: "",
- isSend: true, // 标记为发送中
- createTime: new Date().toISOString(),
- isDeleted: false,
- deleteTime: "",
- sendStatus: 1,
- wechatTime: Date.now(),
- origin: 0,
- msgId: 0,
- recalled: false,
- seq: messageId,
- };
- // 先插入本地数据
- addMessage(localMessage);
+ console.log("handleSend", messageContent);
+ const messageId = +Date.now();
+ // 构造本地消息对象
+ const localMessage: ChatRecord = {
+ id: messageId, // 使用时间戳作为临时ID
+ wechatAccountId: contract.wechatAccountId,
+ wechatFriendId: contract?.chatroomId ? 0 : contract.id,
+ wechatChatroomId: contract?.chatroomId ? contract.id : 0,
+ tenantId: 0,
+ accountId: 0,
+ synergyAccountId: 0,
+ content: messageContent,
+ msgType: 1,
+ msgSubType: 0,
+ msgSvrId: "",
+ isSend: true, // 标记为发送中
+ createTime: new Date().toISOString(),
+ isDeleted: false,
+ deleteTime: "",
+ sendStatus: 1,
+ wechatTime: Date.now(),
+ origin: 0,
+ msgId: 0,
+ recalled: false,
+ seq: messageId,
+ };
+ // 先插入本地数据
+ addMessage(localMessage);
- // 再发送消息到服务器
- const params = {
- wechatAccountId: contract.wechatAccountId,
- wechatChatroomId: contract?.chatroomId ? contract.id : 0,
- wechatFriendId: contract?.chatroomId ? 0 : contract.id,
- msgSubType: 0,
- msgType: 1,
- content: messageContent,
- seq: messageId,
- };
- sendCommand("CmdSendMessage", params);
+ // 再发送消息到服务器
+ const params = {
+ wechatAccountId: contract.wechatAccountId,
+ wechatChatroomId: contract?.chatroomId ? contract.id : 0,
+ wechatFriendId: contract?.chatroomId ? 0 : contract.id,
+ msgSubType: 0,
+ msgType: 1,
+ content: messageContent,
+ seq: messageId,
+ };
+ sendCommand("CmdSendMessage", params);
- // 清空输入框和AI回复内容
- setInputValue("");
- updateQuoteMessageContent("");
- };
+ // 清空输入框和AI回复内容
+ setInputValue("");
+ updateQuoteMessageContent("");
+ },
+ [
+ addMessage,
+ contract.id,
+ contract.wechatAccountId,
+ contract?.chatroomId,
+ inputValue,
+ isLoadingAiChat,
+ updateIsLoadingAiChat,
+ updateQuoteMessageContent,
+ ],
+ );
// AI 消息处理 - 只处理AI辅助模式
// AI接管模式已经在weChat.ts中直接发送,不经过此组件
// 快捷语填充:当 quoteMessageContent 更新时,填充到输入框
useEffect(() => {
if (quoteMessageContent) {
- if (isAiAssist) {
- // AI辅助模式:直接填充输入框
- setInputValue(quoteMessageContent);
- } else {
- // 快捷语模式:直接填充输入框(用户主动点击快捷语,应该替换当前内容)
- setInputValue(quoteMessageContent);
- }
+ // AI辅助模式 & 快捷语模式:都直接填充输入框
+ setInputValue(quoteMessageContent);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [quoteMessageContent, aiQuoteMessageContent, isAiAssist]);
- const handleKeyPress = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey) {
- e.preventDefault();
- handleSend();
- }
- // Ctrl+Enter 换行由 TextArea 自动处理,不需要阻止默认行为
- };
+ const handleKeyPress = useCallback(
+ (e: React.KeyboardEvent) => {
+ // 中文等输入法候选阶段,忽略 Enter,避免误触发送和卡顿感
+ if ((e.nativeEvent as any).isComposing) {
+ return;
+ }
+
+ if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ // Ctrl+Enter 换行由浏览器原生 textarea 处理,不需要阻止默认行为
+ },
+ [handleSend],
+ );
// 处理表情选择
- const handleEmojiSelect = (emoji: EmojiInfo) => {
+ const handleEmojiSelect = useCallback((emoji: EmojiInfo) => {
setInputValue(prevValue => prevValue + `[${emoji.name}]`);
- };
+ }, []);
- // 根据文件格式判断消息类型
- const getMsgTypeByFileFormat = (filePath: string): number => {
- const extension = filePath.toLowerCase().split(".").pop() || "";
+ const handleFileUploaded = useCallback(
+ (
+ filePath: { url: string; name: string; durationMs?: number },
+ fileType: number,
+ ) => {
+ console.log("handleFileUploaded: ", fileType, filePath);
- // 图片格式
- const imageFormats = [
- "jpg",
- "jpeg",
- "png",
- "gif",
- "bmp",
- "webp",
- "svg",
- "ico",
- ];
- if (imageFormats.includes(extension)) {
- return 3; // 图片
- }
-
- // 视频格式
- const videoFormats = [
- "mp4",
- "avi",
- "mov",
- "wmv",
- "flv",
- "mkv",
- "webm",
- "3gp",
- "rmvb",
- ];
- if (videoFormats.includes(extension)) {
- return 43; // 视频
- }
-
- // 其他格式默认为文件
- return 49; // 文件
- };
- const FileType = {
- TEXT: 1,
- IMAGE: 2,
- VIDEO: 3,
- AUDIO: 4,
- FILE: 5,
- };
- const handleFileUploaded = (
- filePath: { url: string; name: string; durationMs?: number },
- fileType: number,
- ) => {
- console.log("handleFileUploaded: ", fileType, filePath);
-
- // msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件)
- let msgType = 1;
- let content: any = "";
- if ([FileType.TEXT].includes(fileType)) {
- msgType = getMsgTypeByFileFormat(filePath.url);
- } else if ([FileType.IMAGE].includes(fileType)) {
- msgType = 3;
- content = filePath.url;
- } else if ([FileType.AUDIO].includes(fileType)) {
- msgType = 34;
- content = JSON.stringify({
- url: filePath.url,
- durationMs: filePath.durationMs,
- });
- } else if ([FileType.FILE].includes(fileType)) {
- msgType = getMsgTypeByFileFormat(filePath.url);
- if (msgType === 3) {
+ // msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件)
+ let msgType = 1;
+ let content: any = "";
+ if ([FileType.TEXT].includes(fileType)) {
+ msgType = getMsgTypeByFileFormat(filePath.url);
+ } else if ([FileType.IMAGE].includes(fileType)) {
+ msgType = 3;
content = filePath.url;
- }
- if (msgType === 43) {
- content = filePath.url;
- }
-
- if (msgType === 49) {
+ } else if ([FileType.AUDIO].includes(fileType)) {
+ msgType = 34;
content = JSON.stringify({
- type: "file",
- title: filePath.name,
url: filePath.url,
+ durationMs: filePath.durationMs,
});
+ } else if ([FileType.FILE].includes(fileType)) {
+ msgType = getMsgTypeByFileFormat(filePath.url);
+ if (msgType === 3) {
+ content = filePath.url;
+ }
+ if (msgType === 43) {
+ content = filePath.url;
+ }
+
+ if (msgType === 49) {
+ content = JSON.stringify({
+ type: "file",
+ title: filePath.name,
+ url: filePath.url,
+ });
+ }
}
- }
- const messageId = +Date.now();
+ const messageId = +Date.now();
- const params = {
- wechatAccountId: contract.wechatAccountId,
- wechatChatroomId: contract?.chatroomId ? contract.id : 0,
- wechatFriendId: contract?.chatroomId ? 0 : contract.id,
- msgSubType: 0,
- msgType,
- content: content,
- seq: messageId,
- };
+ const params = {
+ wechatAccountId: contract.wechatAccountId,
+ wechatChatroomId: contract?.chatroomId ? contract.id : 0,
+ wechatFriendId: contract?.chatroomId ? 0 : contract.id,
+ msgSubType: 0,
+ msgType,
+ content: content,
+ seq: messageId,
+ };
- // 构造本地消息对象
- const localMessage: ChatRecord = {
- id: messageId, // 使用时间戳作为临时ID
- wechatAccountId: contract.wechatAccountId,
- wechatFriendId: contract?.chatroomId ? 0 : contract.id,
- wechatChatroomId: contract?.chatroomId ? contract.id : 0,
- tenantId: 0,
- accountId: 0,
- synergyAccountId: 0,
- content: params.content,
- msgType: msgType,
- msgSubType: 0,
- msgSvrId: "",
- isSend: true, // 标记为发送中
- createTime: new Date().toISOString(),
- isDeleted: false,
- deleteTime: "",
- sendStatus: 1,
- wechatTime: Date.now(),
- origin: 0,
- msgId: 0,
- recalled: false,
- seq: messageId,
- };
- // 先插入本地数据
- addMessage(localMessage);
+ // 构造本地消息对象
+ const localMessage: ChatRecord = {
+ id: messageId, // 使用时间戳作为临时ID
+ wechatAccountId: contract.wechatAccountId,
+ wechatFriendId: contract?.chatroomId ? 0 : contract.id,
+ wechatChatroomId: contract?.chatroomId ? contract.id : 0,
+ tenantId: 0,
+ accountId: 0,
+ synergyAccountId: 0,
+ content: params.content,
+ msgType: msgType,
+ msgSubType: 0,
+ msgSvrId: "",
+ isSend: true, // 标记为发送中
+ createTime: new Date().toISOString(),
+ isDeleted: false,
+ deleteTime: "",
+ sendStatus: 1,
+ wechatTime: Date.now(),
+ origin: 0,
+ msgId: 0,
+ recalled: false,
+ seq: messageId,
+ };
+ // 先插入本地数据
+ addMessage(localMessage);
- sendCommand("CmdSendMessage", params);
- };
+ sendCommand("CmdSendMessage", params);
+ },
+ [addMessage, contract.wechatAccountId, contract.chatroomId, contract.id],
+ );
- const handleCancelAction = () => {
+ const handleCancelAction = useCallback(() => {
+ if (!EnterModule) return;
updateShowCheckbox(false);
updateEnterModule("common");
- };
- const handTurnRignt = () => {
+ }, [EnterModule, updateShowCheckbox, updateEnterModule]);
+
+ const handTurnRignt = useCallback(() => {
setTransmitModal(true);
- };
- const openChatRecordModel = () => {
+ }, [setTransmitModal]);
+
+ const openChatRecordModel = useCallback(() => {
updateShowChatRecordModel(!showChatRecordModel);
- };
+ }, [showChatRecordModel, updateShowChatRecordModel]);
const [mapVisible, setMapVisible] = useState(false);
+ const handleOpenMap = useCallback(() => {
+ setMapVisible(true);
+ }, []);
+
+ const handleAudioUploaded = useCallback(
+ (audioData: { name: string; url: string; durationMs?: number }) => {
+ handleFileUploaded(
+ {
+ name: audioData.name,
+ url: audioData.url,
+ durationMs: audioData.durationMs,
+ },
+ FileType.AUDIO,
+ );
+ },
+ [handleFileUploaded],
+ );
+
return (
<>
{/* 聊天输入 */}
@@ -394,96 +533,27 @@ const MessageEnter: React.FC = ({ contract }) => {
<>
{["common"].includes(EnterModule) && (
-
-
-
-
- handleFileUploaded(fileInfo, FileType.FILE)
- }
- maxSize={10}
- type={4}
- slot={
- }
- />
- }
- />
-
- handleFileUploaded(fileInfo, FileType.IMAGE)
- }
- maxSize={10}
- type={1}
- slot={
- }
- />
- }
- />
-
-
- handleFileUploaded(
- {
- name: audioData.name,
- url: audioData.url,
- durationMs: audioData.durationMs,
- },
- FileType.AUDIO,
- )
- }
- className={styles.toolbarButton}
- />
- }
- onClick={() => setMapVisible(true)}
- />
-
- {/* AI模式下显示重新生成按钮 */}
- {(isAiAssist || isAiTakeover) && (
-
- }
- onClick={handleManualTriggerAi}
- disabled={isLoadingAiChat}
- />
-
- )}
-
-
-
+
-
@@ -524,7 +594,7 @@ const MessageEnter: React.FC
= ({ contract }) => {
>
)}
- setMapVisible(false)}
contract={contract}
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
index c534fe9d..05d09d01 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
@@ -1,4 +1,11 @@
-import React, { CSSProperties, useEffect, useRef, useState } from "react";
+import React, {
+ CSSProperties,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
import { Avatar, Checkbox } from "antd";
import { UserOutlined, LoadingOutlined } from "@ant-design/icons";
import AudioMessage from "./components/AudioMessage/AudioMessage";
@@ -148,6 +155,161 @@ type GroupRenderItem = {
wechatId?: string;
};
+interface MessageItemProps {
+ msg: ChatRecord;
+ contract: ContractData | weChatGroup;
+ isGroup: boolean;
+ showCheckbox: boolean;
+ isSelected: boolean;
+ currentCustomerAvatar?: string;
+ renderGroupUser: (msg: ChatRecord) => { avatar: string; nickname: string };
+ clearWechatidInContent: (sender: any, content: string) => string;
+ parseMessageContent: (
+ content: string | null | undefined,
+ msg: ChatRecord,
+ msgType?: number,
+ ) => React.ReactNode;
+ onCheckboxChange: (checked: boolean, msg: ChatRecord) => void;
+ onContextMenu: (e: React.MouseEvent, msg: ChatRecord, isOwn: boolean) => void;
+}
+
+const MessageItem: React.FC = React.memo(
+ ({
+ msg,
+ contract,
+ isGroup,
+ showCheckbox,
+ isSelected,
+ currentCustomerAvatar,
+ renderGroupUser,
+ clearWechatidInContent,
+ parseMessageContent,
+ onCheckboxChange,
+ onContextMenu,
+ }) => {
+ if (!msg) return null;
+
+ const isOwn = msg?.isSend;
+
+ return (
+ onContextMenu(e, msg, !!isOwn)}
+ >
+
+ {/* 单聊,自己不是发送方 */}
+ {!isGroup && !isOwn && (
+ <>
+ {showCheckbox && (
+
+ onCheckboxChange(e.target.checked, msg)}
+ />
+
+ )}
+
}
+ className={styles.messageAvatar}
+ />
+
+ {!isOwn && (
+
+ {contract.nickname}
+
+ )}
+ <>{parseMessageContent(msg?.content, msg, msg?.msgType)}>
+
+ >
+ )}
+
+ {/* 群聊,自己不是发送方 */}
+ {isGroup && !isOwn && (
+ <>
+ {showCheckbox && (
+
+ onCheckboxChange(e.target.checked, msg)}
+ />
+
+ )}
+
}
+ className={styles.messageAvatar}
+ />
+
+ {!isOwn && (
+
+ {renderGroupUser(msg)?.nickname}
+
+ )}
+ <>
+ {parseMessageContent(
+ clearWechatidInContent(msg?.sender, msg?.content),
+ msg,
+ msg?.msgType,
+ )}
+ >
+
+ >
+ )}
+
+ {/* 自己发送的消息 */}
+ {!!isOwn && (
+ <>
+ {showCheckbox && (
+
+ onCheckboxChange(e.target.checked, msg)}
+ />
+
+ )}
+
}
+ className={styles.messageAvatar}
+ />
+
{parseMessageContent(msg?.content, msg, msg?.msgType)}
+ {msg.sendStatus === 1 && (
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+ },
+ (prev, next) =>
+ prev.msg === next.msg &&
+ prev.isGroup === next.isGroup &&
+ prev.showCheckbox === next.showCheckbox &&
+ prev.isSelected === next.isSelected &&
+ prev.currentCustomerAvatar === next.currentCustomerAvatar &&
+ prev.contract === next.contract,
+);
+
+MessageItem.displayName = "MessageItem";
+
const MessageRecord: React.FC = ({ contract }) => {
const messagesEndRef = useRef(null);
const messagesContainerRef = useRef(null);
@@ -204,7 +366,7 @@ const MessageRecord: React.FC = ({ contract }) => {
};
// 解析表情包文字格式[表情名称]并替换为img标签
- const parseEmojiText = (text: string): React.ReactNode[] => {
+ const parseEmojiText = useCallback((text: string): React.ReactNode[] => {
const emojiRegex = /\[([^\]]+)\]/g;
const parts: React.ReactNode[] = [];
let lastIndex = 0;
@@ -252,143 +414,153 @@ const MessageRecord: React.FC = ({ contract }) => {
}
return parts;
- };
+ }, []);
- const renderUnknownContent = (
- rawContent: string,
- trimmedContent: string,
- msg?: ChatRecord,
- contract?: ContractData | weChatGroup,
- ) => {
- if (isLegacyEmojiContent(trimmedContent)) {
- return renderEmojiContent(rawContent);
- }
-
- const jsonData = tryParseContentJson(trimmedContent);
-
- if (jsonData && typeof jsonData === "object") {
- // 判断是否为红包消息
- if (
- jsonData.nativeurl &&
- typeof jsonData.nativeurl === "string" &&
- jsonData.nativeurl.includes(
- "wxpay://c2cbizmessagehandler/hongbao/receivehongbao",
- )
- ) {
- return (
-
- );
+ const renderUnknownContent = useCallback(
+ (
+ rawContent: string,
+ trimmedContent: string,
+ msg?: ChatRecord,
+ contractParam?: ContractData | weChatGroup,
+ ) => {
+ if (isLegacyEmojiContent(trimmedContent)) {
+ return renderEmojiContent(rawContent);
}
- // 判断是否为转账消息
- if (
- jsonData.title === "微信转账" ||
- (jsonData.transferid && jsonData.feedesc)
- ) {
- return (
-
- );
- }
+ const jsonData = tryParseContentJson(trimmedContent);
- if (jsonData.type === "file" && msg && contract) {
- return (
-
- );
- }
+ if (jsonData && typeof jsonData === "object") {
+ // 判断是否为红包消息
+ if (
+ jsonData.nativeurl &&
+ typeof jsonData.nativeurl === "string" &&
+ jsonData.nativeurl.includes(
+ "wxpay://c2cbizmessagehandler/hongbao/receivehongbao",
+ )
+ ) {
+ return (
+
+ );
+ }
- if (jsonData.type === "link" && jsonData.title && jsonData.url) {
- const { title, desc, thumbPath, url } = jsonData;
+ // 判断是否为转账消息
+ if (
+ jsonData.title === "微信转账" ||
+ (jsonData.transferid && jsonData.feedesc)
+ ) {
+ return (
+
+ );
+ }
- return (
-
+ if (jsonData.type === "file" && msg && contractParam) {
+ return (
+
+ );
+ }
+
+ if (jsonData.type === "link" && jsonData.title && jsonData.url) {
+ const { title, desc, thumbPath, url } = jsonData;
+
+ return (
openInNewTab(url)}
+ className={`${styles.miniProgramMessage} ${styles.miniProgramType1}`}
>
- {thumbPath && (
+
openInNewTab(url)}
+ >
+ {thumbPath && (
+

{
+ const target = event.target as HTMLImageElement;
+ target.style.display = "none";
+ }}
+ />
+ )}
+
+
{title}
+ {desc &&
{desc}
}
+
+
+
链接
+
+ );
+ }
+
+ if (
+ jsonData.previewImage &&
+ (jsonData.tencentUrl || jsonData.videoUrl)
+ ) {
+ const previewImageUrl = String(jsonData.previewImage).replace(
+ /[`"']/g,
+ "",
+ );
+ return (
+
+

{
+ const videoUrl = jsonData.videoUrl || jsonData.tencentUrl;
+ if (videoUrl) {
+ openInNewTab(videoUrl);
+ }
+ }}
onError={event => {
const target = event.target as HTMLImageElement;
- target.style.display = "none";
+ const parent = target.parentElement?.parentElement;
+ if (parent) {
+ parent.innerHTML = `
[视频预览加载失败]
`;
+ }
}}
/>
- )}
-
-
{title}
- {desc &&
{desc}
}
+
-
链接
-
- );
+ );
+ }
}
- if (jsonData.previewImage && (jsonData.tencentUrl || jsonData.videoUrl)) {
- const previewImageUrl = String(jsonData.previewImage).replace(
- /[`"']/g,
- "",
- );
- return (
-
-
-

{
- const videoUrl = jsonData.videoUrl || jsonData.tencentUrl;
- if (videoUrl) {
- openInNewTab(videoUrl);
- }
- }}
- onError={event => {
- const target = event.target as HTMLImageElement;
- const parent = target.parentElement?.parentElement;
- if (parent) {
- parent.innerHTML = `
[视频预览加载失败]
`;
- }
- }}
- />
-
-
-
- );
+ if (isHttpImageUrl(trimmedContent)) {
+ return renderImageContent({
+ src: rawContent,
+ alt: "图片消息",
+ fallbackText: "[图片加载失败]",
+ });
}
- }
- if (isHttpImageUrl(trimmedContent)) {
- return renderImageContent({
- src: rawContent,
- alt: "图片消息",
- fallbackText: "[图片加载失败]",
- });
- }
+ if (isFileUrl(trimmedContent)) {
+ return renderFileContent(trimmedContent);
+ }
- if (isFileUrl(trimmedContent)) {
- return renderFileContent(trimmedContent);
- }
-
- return (
-
{parseEmojiText(rawContent)}
- );
- };
+ return (
+
{parseEmojiText(rawContent)}
+ );
+ },
+ [parseEmojiText],
+ );
useEffect(() => {
const fetchGroupMembers = async () => {
@@ -407,21 +579,33 @@ const MessageRecord: React.FC
= ({ contract }) => {
fetchGroupMembers();
}, [contract.id, contract.chatroomId]);
- const renderGroupUser = (msg: ChatRecord) => {
- if (!msg) {
- return { avatar: "", nickname: "" };
- }
+ const groupMemberMap = useMemo(() => {
+ const map = new Map();
+ groupRender.forEach(member => {
+ if (member?.identifier) {
+ map.set(member.identifier, member);
+ }
+ });
+ return map;
+ }, [groupRender]);
- const member = groupRender.find(
- user => user?.identifier === msg?.senderWechatId,
- );
- console.log(member, "member");
+ const renderGroupUser = useCallback(
+ (msg: ChatRecord) => {
+ if (!msg) {
+ return { avatar: "", nickname: "" };
+ }
- return {
- avatar: member?.avatar || msg?.avatar,
- nickname: member?.nickname || msg?.senderNickname,
- };
- };
+ const member = msg.senderWechatId
+ ? groupMemberMap.get(msg.senderWechatId)
+ : undefined;
+
+ return {
+ avatar: member?.avatar || msg?.avatar,
+ nickname: member?.nickname || msg?.senderNickname,
+ };
+ },
+ [groupMemberMap],
+ );
useEffect(() => {
const prevMessages = prevMessagesRef.current;
@@ -596,29 +780,28 @@ const MessageRecord: React.FC = ({ contract }) => {
// 获取群成员头像
// 清理微信ID前缀
- const clearWechatidInContent = (sender: any, content: string) => {
+ const clearWechatidInContent = useCallback((sender: any, content: string) => {
try {
return content.replace(new RegExp(`${sender?.wechatId}:\n`, "g"), "");
} catch (err) {
return "-";
}
- };
+ }, []);
// 右键菜单事件处理
- const handleContextMenu = (
- e: React.MouseEvent,
- msg: ChatRecord,
- isOwn: boolean,
- ) => {
- e.preventDefault();
- setContextMenu({
- visible: true,
- x: e.clientX,
- y: e.clientY,
- messageData: msg,
- });
- setNowIsOwn(isOwn);
- };
+ const handleContextMenu = useCallback(
+ (e: React.MouseEvent, msg: ChatRecord, isOwn: boolean) => {
+ e.preventDefault();
+ setContextMenu({
+ visible: true,
+ x: e.clientX,
+ y: e.clientY,
+ messageData: msg,
+ });
+ setNowIsOwn(isOwn);
+ },
+ [],
+ );
const handleCloseContextMenu = () => {
setContextMenu({
@@ -630,16 +813,21 @@ const MessageRecord: React.FC = ({ contract }) => {
};
// 处理checkbox选中状态变化
- const handleCheckboxChange = (checked: boolean, msg: ChatRecord) => {
- if (checked) {
- // 添加到选中记录
- setSelectedRecords(prev => [...prev, msg]);
- } else {
- // 从选中记录中移除
- setSelectedRecords(prev => prev.filter(record => record.id !== msg.id));
- }
- updateSelectedChatRecords(selectedRecords);
- };
+ const handleCheckboxChange = useCallback(
+ (checked: boolean, msg: ChatRecord) => {
+ setSelectedRecords(prev => {
+ let next: ChatRecord[];
+ if (checked) {
+ next = [...prev, msg];
+ } else {
+ next = prev.filter(record => record.id !== msg.id);
+ }
+ updateSelectedChatRecords(next);
+ return next;
+ });
+ },
+ [updateSelectedChatRecords],
+ );
// 检查消息是否被选中
const isMessageSelected = (msg: ChatRecord) => {
@@ -662,124 +850,12 @@ const MessageRecord: React.FC = ({ contract }) => {
}));
};
- // 渲染单条消息
- const renderMessage = (msg: ChatRecord) => {
- // 添加null检查,防止访问null对象的属性
- if (!msg) return null;
+ const groupedMessages = useMemo(
+ () => groupMessagesByTime(currentMessages),
+ [currentMessages],
+ );
- const isOwn = msg?.isSend;
- const isGroup = !!contract.chatroomId;
-
- return (
- handleContextMenu(e, msg, isOwn)}
- >
-
- {/* 如果不是群聊 */}
- {!isGroup && !isOwn && (
- <>
- {/* Checkbox 显示控制 */}
- {showCheckbox && (
-
- handleCheckboxChange(e.target.checked, msg)}
- />
-
- )}
-
}
- className={styles.messageAvatar}
- />
-
- {!isOwn && (
-
- {contract.nickname}
-
- )}
- <>{parseMessageContent(msg?.content, msg, msg?.msgType)}>
-
- >
- )}
- {/* 如果是群聊 */}
- {isGroup && !isOwn && (
- <>
- {/* 群聊场景下根据消息发送者匹配头像与昵称 */}
- {showCheckbox && (
-
- handleCheckboxChange(e.target.checked, msg)}
- />
-
- )}
-
}
- className={styles.messageAvatar}
- />
-
- {!isOwn && (
-
- {renderGroupUser(msg)?.nickname}
-
- )}
- <>
- {parseMessageContent(
- clearWechatidInContent(msg?.sender, msg?.content),
- msg,
- msg?.msgType,
- )}
- >
-
- >
- )}
- {!!isOwn && (
- <>
- {/* Checkbox 显示控制 */}
- {showCheckbox && (
-
- handleCheckboxChange(e.target.checked, msg)}
- />
-
- )}
-
}
- className={styles.messageAvatar}
- />
-
{parseMessageContent(msg?.content, msg, msg?.msgType)}
- {/* 发送状态 loading 图标 */}
- {msg.sendStatus === 1 && (
-
-
-
- )}
- >
- )}
-
-
- );
- };
+ const isGroupChat = !!contract.chatroomId;
const loadMoreMessages = () => {
if (messagesLoading || !currentMessagesHasMore) {
return;
@@ -871,7 +947,7 @@ const MessageRecord: React.FC = ({ contract }) => {
loadMoreMessages()}
+ onClick={loadMoreMessages}
style={{
cursor:
currentMessagesHasMore && !messagesLoading ? "pointer" : "default",
@@ -881,7 +957,7 @@ const MessageRecord: React.FC = ({ contract }) => {
{currentMessagesHasMore ? "点击加载更早的信息" : "已经没有更早的消息了"}
{messagesLoading ? : ""}
- {groupMessagesByTime(currentMessages).map((group, groupIndex) => (
+ {groupedMessages.map((group, groupIndex) => (
{group.messages
.filter(v => [10000, -10001].includes(v.msgType))
@@ -923,7 +999,23 @@ const MessageRecord: React.FC = ({ contract }) => {
{group.messages
.filter(v => ![10000, 570425393, 90000, -10001].includes(v.msgType))
.map(msg => {
- return renderMessage(msg);
+ if (!msg) return null;
+ return (
+
+ );
})}
))}
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx
index 7e7b4d41..013ec6d9 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/index.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useCallback, useMemo } from "react";
import {
Layout,
Button,
@@ -60,104 +60,119 @@ const ChatWindow: React.FC
= ({ contract }) => {
const [followupModalVisible, setFollowupModalVisible] = useState(false);
const [todoModalVisible, setTodoModalVisible] = useState(false);
- const onToggleProfile = () => {
- setShowProfile(!showProfile);
- };
+ const onToggleProfile = useCallback(() => {
+ setShowProfile(prev => !prev);
+ }, []);
- const handleFollowupClick = () => {
+ const handleFollowupClick = useCallback(() => {
setFollowupModalVisible(true);
- };
+ }, []);
- const handleFollowupModalClose = () => {
+ const handleFollowupModalClose = useCallback(() => {
setFollowupModalVisible(false);
- };
+ }, []);
- const handleTodoClick = () => {
+ const handleTodoClick = useCallback(() => {
setTodoModalVisible(true);
- };
+ }, []);
- const handleTodoModalClose = () => {
+ const handleTodoModalClose = useCallback(() => {
setTodoModalVisible(false);
- };
+ }, []);
const [currentConfig, setCurrentConfig] = useState(
- typeOptions.find(option => option.value === aiQuoteMessageContent),
+ typeOptions.find(option => option.value === aiQuoteMessageContent) ||
+ typeOptions[0],
);
useEffect(() => {
- setCurrentConfig(
- typeOptions.find(option => option.value === aiQuoteMessageContent),
- );
+ const found =
+ typeOptions.find(option => option.value === aiQuoteMessageContent) ||
+ typeOptions[0];
+ setCurrentConfig(found);
}, [aiQuoteMessageContent]);
// 处理配置选择
- const handleConfigChange = async option => {
- setCurrentConfig({
- value: option.value,
- label: option.label,
- });
-
- try {
- // 1. 保存配置到后端
- await setFriendInjectConfig({
- type: option.value,
- wechatAccountId: contract.wechatAccountId,
- friendId: contract.id,
+ const handleConfigChange = useCallback(
+ async (option: { value: number; label: string }) => {
+ setCurrentConfig({
+ value: option.value,
+ label: option.label,
});
- // 2. 更新 Store 中的 AI 配置状态
- updateAiQuoteMessageContent(option.value);
+ try {
+ // 1. 保存配置到后端
+ await setFriendInjectConfig({
+ type: option.value,
+ wechatAccountId: contract.wechatAccountId,
+ friendId: contract.id,
+ });
- // 3. 确定联系人类型
- const contactType: "friend" | "group" = contract.chatroomId
- ? "group"
- : "friend";
- const aiType = option.value;
+ // 2. 更新 Store 中的 AI 配置状态
+ updateAiQuoteMessageContent(option.value);
- console.log(
- `开始更新 aiType: contactId=${contract.id}, type=${contactType}, aiType=${aiType}`,
- );
+ // 3. 确定联系人类型
+ const contactType: "friend" | "group" = contract.chatroomId
+ ? "group"
+ : "friend";
+ const aiType = option.value;
- // 4. 更新会话列表数据库的 aiType
- await MessageManager.updateSession({
- userId: currentUserId,
- id: contract.id!,
- type: contactType,
- aiType: aiType,
- });
- console.log("✅ 会话列表数据库 aiType 更新成功");
+ console.log(
+ `开始更新 aiType: contactId=${contract.id}, type=${contactType}, aiType=${aiType}`,
+ );
- // 5. 更新联系人数据库的 aiType
- const contactInDb = await ContactManager.getContactByIdAndType(
- currentUserId,
- contract.id!,
- contactType,
- );
-
- if (contactInDb) {
- await ContactManager.updateContact({
- ...contactInDb,
+ // 4. 更新会话列表数据库的 aiType
+ await MessageManager.updateSession({
+ userId: currentUserId,
+ id: contract.id!,
+ type: contactType,
aiType: aiType,
});
- console.log("✅ 联系人数据库 aiType 更新成功");
- } else {
- console.warn("⚠️ 联系人数据库中未找到该联系人");
+ console.log("✅ 会话列表数据库 aiType 更新成功");
+
+ // 5. 更新联系人数据库的 aiType
+ const contactInDb = await ContactManager.getContactByIdAndType(
+ currentUserId,
+ contract.id!,
+ contactType,
+ );
+
+ if (contactInDb) {
+ await ContactManager.updateContact({
+ ...contactInDb,
+ aiType: aiType,
+ });
+ console.log("✅ 联系人数据库 aiType 更新成功");
+ } else {
+ console.warn("⚠️ 联系人数据库中未找到该联系人");
+ }
+
+ // 6. 更新 Store 中的 currentContract(通过重新设置)
+ const updatedContract = {
+ ...contract,
+ aiType: aiType,
+ };
+ setCurrentContact(updatedContract);
+ console.log("✅ Store currentContract aiType 更新成功");
+
+ message.success(`已切换为${option.label}`);
+ } catch (error) {
+ console.error("更新 AI 配置失败:", error);
+ message.error("配置更新失败,请重试");
}
+ },
+ [contract, currentUserId, setCurrentContact, updateAiQuoteMessageContent],
+ );
- // 6. 更新 Store 中的 currentContract(通过重新设置)
- const updatedContract = {
- ...contract,
- aiType: aiType,
- };
- setCurrentContact(updatedContract);
- console.log("✅ Store currentContract aiType 更新成功");
-
- message.success(`已切换为${option.label}`);
- } catch (error) {
- console.error("更新 AI 配置失败:", error);
- message.error("配置更新失败,请重试");
- }
- };
+ const aiTypeMenuItems = useMemo(
+ () =>
+ typeOptions.map(option => ({
+ key: option.value,
+ label: option.label,
+ onClick: () => handleConfigChange(option),
+ })),
+ [handleConfigChange],
+ );
return (
@@ -183,11 +198,7 @@ const ChatWindow: React.FC = ({ contract }) => {
{!contract.chatroomId && (
({
- key: option.value,
- label: option.label,
- onClick: () => handleConfigChange(option),
- })),
+ items: aiTypeMenuItems,
}}
trigger={["click"]}
placement="bottomRight"
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx
index 52ffa850..b8dce346 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx
@@ -30,6 +30,56 @@ import { messageFilter } from "@/utils/filter";
import { ChatSession } from "@/utils/db";
interface MessageListProps {}
+interface SessionItemProps {
+ session: ChatSession;
+ isActive: boolean;
+ onClick: (session: ChatSession) => void;
+ onContextMenu: (e: React.MouseEvent, session: ChatSession) => void;
+}
+
+const SessionItem: React.FC = React.memo(
+ ({ session, isActive, onClick, onContextMenu }) => {
+ return (
+ onClick(session)}
+ onContextMenu={e => onContextMenu(e, session)}
+ >
+
+
+ :
+ }
+ />
+
+
+
+
+ {session.conRemark || session.nickname || session.wechatId}
+
+
+ {formatWechatTime(session?.lastUpdateTime)}
+
+
+
+ {messageFilter(session.content)}
+
+
+
+
+ );
+ },
+ (prev, next) =>
+ prev.isActive === next.isActive && prev.session === next.session,
+) as React.FC;
+
+SessionItem.displayName = "SessionItem";
+
const MessageList: React.FC = () => {
const searchKeyword = useContactStore(state => state.searchKeyword);
const { setCurrentContact, currentContract } = useWeChatStore();
@@ -630,10 +680,10 @@ const MessageList: React.FC = () => {
);
}
- // 根据搜索关键词进行模糊匹配(支持搜索昵称、备注名、微信号)
- if (searchKeyword.trim()) {
- const keyword = searchKeyword.toLowerCase();
+ const keyword = searchKeyword.trim().toLowerCase();
+ // 根据搜索关键词进行模糊匹配(支持搜索昵称、备注名、微信号)
+ if (keyword) {
// 如果搜索关键词可能是微信号,需要从联系人表补充 wechatId
const sessionsNeedingWechatId = filtered.filter(
v => !v.wechatId && v.type === "friend",
@@ -682,7 +732,16 @@ const MessageList: React.FC = () => {
setFilteredSessions(filtered);
};
- filterSessions();
+ // 搜索过滤做简单防抖,减少频繁重算
+ const timer = window.setTimeout(() => {
+ filterSessions();
+ }, 200);
+
+ return () => {
+ if (timer) {
+ window.clearTimeout(timer);
+ }
+ };
}, [sessions, currentCustomer, searchKeyword, currentUserId]);
// 渲染完毕后自动点击第一个聊天记录
@@ -989,43 +1048,13 @@ const MessageList: React.FC = () => {
(
- onContactClick(session)}
- onContextMenu={e => handleContextMenu(e, session)}
- >
-
-
-
- ) : (
-
- )
- }
- />
-
-
-
-
- {session.conRemark || session.nickname || session.wechatId}
-
-
- {formatWechatTime(session?.lastUpdateTime)}
-
-
-
- {messageFilter(session.content)}
-
-
-
-
+ session={session}
+ isActive={!!currentContract && currentContract.id === session.id}
+ onClick={onContactClick}
+ onContextMenu={handleContextMenu}
+ />
)}
locale={{
emptyText: