diff --git a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss new file mode 100644 index 00000000..cfd65086 --- /dev/null +++ b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/MessageRecord.module.scss @@ -0,0 +1,238 @@ +// 消息容器 +.messagesContainer { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; + background: #f5f5f5; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: #d9d9d9; + border-radius: 3px; + + &:hover { + background: #bfbfbf; + } + } +} + +// 时间分组 +.messageTime { + text-align: center; + color: #8c8c8c; + font-size: 12px; + margin: 8px 0; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background: #e8e8e8; + z-index: 0; + } + + &::after { + content: attr(data-time); + background: #f5f5f5; + padding: 0 12px; + position: relative; + z-index: 1; + } +} + +// 消息项 +.messageItem { + display: flex; + margin-bottom: 12px; + + &.ownMessage { + justify-content: flex-end; + + .messageContent { + flex-direction: row-reverse; + } + + .messageBubble { + background: #1890ff; + color: white; + border-radius: 18px 4px 18px 18px; + } + } + + &.otherMessage { + justify-content: flex-start; + + .messageBubble { + background: white; + color: #262626; + border-radius: 4px 18px 18px 18px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + } +} + +// 消息内容容器 +.messageContent { + display: flex; + align-items: flex-start; + gap: 8px; + max-width: 70%; +} + +// 头像 +.messageAvatar { + flex-shrink: 0; + margin-top: 2px; +} + +// 消息气泡 +.messageBubble { + padding: 8px 12px; + max-width: 100%; + word-wrap: break-word; + position: relative; +} + +// 发送者名称 +.messageSender { + font-size: 12px; + color: #8c8c8c; + margin-bottom: 4px; + font-weight: 500; +} + +// 普通文本消息 +.messageText { + line-height: 1.4; + white-space: pre-wrap; + word-break: break-word; +} + +// 表情包消息 +.emojiMessage { + img { + border-radius: 8px; + cursor: pointer; + transition: transform 0.2s; + + &:hover { + transform: scale(1.05); + } + } +} + +// 视频消息 +.videoMessage { + .videoContainer { + position: relative; + display: inline-block; + border-radius: 8px; + overflow: hidden; + cursor: pointer; + transition: transform 0.2s; + + &:hover { + transform: scale(1.02); + + .playButton { + background: rgba(0, 0, 0, 0.8); + } + } + } + + .videoPreview { + max-width: 200px; + max-height: 200px; + display: block; + } + + .playButton { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 48px; + height: 48px; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + + svg { + margin-left: 2px; // 视觉居中调整 + } + } +} + +// 文件消息样式(如果需要) +.fileMessage { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + border: 1px solid #d9d9d9; + border-radius: 6px; + background: #fafafa; + cursor: pointer; + transition: background 0.2s; + + &:hover { + background: #f0f0f0; + } + + .fileIcon { + font-size: 24px; + color: #1890ff; + } + + .fileInfo { + flex: 1; + min-width: 0; + + .fileName { + font-weight: 500; + color: #262626; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .fileSize { + font-size: 12px; + color: #8c8c8c; + margin-top: 2px; + } + } +} + +// 响应式设计 +@media (max-width: 768px) { + .messageContent { + max-width: 85%; + } + + .messageBubble { + padding: 6px 10px; + } + + .videoMessage .videoPreview { + max-width: 150px; + max-height: 150px; + } +} \ No newline at end of file diff --git a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/index.tsx b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/index.tsx new file mode 100644 index 00000000..40fd62cf --- /dev/null +++ b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/components/MessageRecord/index.tsx @@ -0,0 +1,203 @@ +import React from "react"; +import { Avatar } from "antd"; +import { UserOutlined } from "@ant-design/icons"; +import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; +import { formatWechatTime } from "@/utils/common"; +import styles from "./MessageRecord.module.scss"; + +interface MessageRecordProps { + messages: ChatRecord[]; + contract: ContractData | weChatGroup; + currentGroupMembers?: any[]; + messagesEndRef?: React.RefObject; +} + +const MessageRecord: React.FC = ({ + messages, + contract, + currentGroupMembers = [], + messagesEndRef, +}) => { + // 解析消息内容,判断消息类型并返回对应的渲染内容 + const parseMessageContent = ( + content: string | null | undefined, + msg: ChatRecord, + ) => { + // 处理null或undefined的内容 + if (content === null || content === undefined) { + return
消息内容不可用
; + } + // 检查是否为表情包 + if ( + typeof content === "string" && + content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") && + content.includes("#") + ) { + return ( +
+ 表情包 window.open(content, "_blank")} + /> +
+ ); + } + + // 检查是否为带预览图的视频消息 + try { + if ( + typeof content === "string" && + content.trim().startsWith("{") && + content.trim().endsWith("}") + ) { + const videoData = JSON.parse(content); + // 处理视频消息格式 {"previewImage":"https://...", "tencentUrl":"...", "videoUrl":"...", "isLoading":true} + if (videoData.previewImage && videoData.tencentUrl) { + // 提取预览图URL,去掉可能的引号 + const previewImageUrl = videoData.previewImage.replace(/[`"']/g, ""); + + return ( +
+
+ 视频预览 { + if (videoData.videoUrl) { + window.open(videoData.videoUrl, "_blank"); + } else if (videoData.tencentUrl) { + window.open(videoData.tencentUrl, "_blank"); + } + }} + /> +
+ + + +
+
+
+ ); + } + } + } catch (e) { + // JSON解析失败,按普通文本处理 + } + + // 普通文本消息 + return
{content}
; + }; + + // 获取群成员头像 + const groupMemberAvatar = (msg: ChatRecord) => { + const groupMembers = currentGroupMembers.find( + v => v?.wechatId == msg?.sender?.wechatId, + ); + return groupMembers?.avatar; + }; + + // 清理微信ID前缀 + const clearWechatidInContent = (sender: any, content: string) => { + try { + return content.replace(new RegExp(`${sender?.wechatId}:\n`, "g"), ""); + } catch (err) { + return "-"; + } + }; + + // 用于分组消息并添加时间戳的辅助函数 + const groupMessagesByTime = (messages: ChatRecord[]) => { + return messages + .filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息 + .map(msg => ({ + time: formatWechatTime(msg?.wechatTime), + messages: [msg], + })); + }; + + // 渲染单条消息 + const renderMessage = (msg: ChatRecord) => { + // 添加null检查,防止访问null对象的属性 + if (!msg) return null; + + const isOwn = msg?.isSend; + const isGroup = !!contract.chatroomId; + return ( +
+
+ {/* 如果不是群聊 */} + {!isGroup && !isOwn && ( + <> + } + className={styles.messageAvatar} + /> + +
+ {!isOwn && ( +
+ {contract.nickname} +
+ )} + {parseMessageContent(msg?.content, msg)} +
+ + )} + {/* 如果是群聊 */} + {isGroup && !isOwn && ( + <> + } + className={styles.messageAvatar} + /> + +
+ {!isOwn && ( +
+ {msg?.sender?.nickname} +
+ )} + {parseMessageContent( + clearWechatidInContent(msg?.sender, msg?.content), + msg, + )} +
+ + )} + + {isOwn && ( +
+ {parseMessageContent(msg?.content, msg)} +
+ )} +
+
+ ); + }; + + return ( +
+ {groupMessagesByTime(messages).map((group, groupIndex) => ( + +
{group.time}
+ {group.messages.map(renderMessage)} +
+ ))} +
+
+ ); +}; + +export default MessageRecord; diff --git a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/index.tsx b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/index.tsx index 0d107a46..16a3c643 100644 --- a/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/index.tsx +++ b/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/index.tsx @@ -5,23 +5,14 @@ import { VideoCameraOutlined, MoreOutlined, UserOutlined, - DownloadOutlined, - FileOutlined, - FilePdfOutlined, - FileWordOutlined, - FileExcelOutlined, - FilePptOutlined, - PlayCircleFilled, TeamOutlined, - FolderOutlined, - EnvironmentOutlined, } from "@ant-design/icons"; import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data"; import styles from "./ChatWindow.module.scss"; -import { useWebSocketStore } from "@/store/module/websocket/websocket"; -import { formatWechatTime } from "@/utils/common"; + import ProfileCard from "./components/ProfileCard"; import MessageEnter from "./components/MessageEnter"; +import MessageRecord from "./components/MessageRecord"; import { useWeChatStore } from "@/store/module/weChat/weChat"; const { Header, Content } = Layout; @@ -41,586 +32,15 @@ const ChatWindow: React.FC = ({ const currentGroupMembers = useWeChatStore( state => state.currentGroupMembers, ); - const prevMessagesRef = useRef(currentMessages); useEffect(() => { - const prevMessages = prevMessagesRef.current; - - const hasVideoStateChange = currentMessages.some((msg, index) => { - // 首先检查消息对象本身是否为null或undefined - if (!msg || !msg.content) return false; - - const prevMsg = prevMessages[index]; - if (!prevMsg || !prevMsg.content || prevMsg.id !== msg.id) return false; - - try { - const currentContent = - typeof msg.content === "string" - ? JSON.parse(msg.content) - : msg.content; - const prevContent = - typeof prevMsg.content === "string" - ? JSON.parse(prevMsg.content) - : prevMsg.content; - - // 检查视频状态是否发生变化(开始加载、完成加载、获得URL) - const currentHasVideo = - currentContent.previewImage && currentContent.tencentUrl; - const prevHasVideo = prevContent.previewImage && prevContent.tencentUrl; - - if (currentHasVideo && prevHasVideo) { - // 检查加载状态变化或视频URL变化 - return ( - currentContent.isLoading !== prevContent.isLoading || - currentContent.videoUrl !== prevContent.videoUrl - ); - } - - return false; - } catch (e) { - return false; - } - }); - - // 只有在没有视频状态变化时才自动滚动到底部 - if (!hasVideoStateChange) { - scrollToBottom(); - } - - // 更新上一次的消息状态 - prevMessagesRef.current = currentMessages; + scrollToBottom(); }, [currentMessages]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; - // 处理视频播放请求,发送socket请求获取真实视频地址 - const handleVideoPlayRequest = (tencentUrl: string, messageId: number) => { - console.log("发送视频下载请求:", { messageId, tencentUrl }); - - // 先设置加载状态 - useWeChatStore.getState().setVideoLoading(messageId, true); - - // 构建socket请求数据 - useWebSocketStore.getState().sendCommand("CmdDownloadVideo", { - chatroomMessageId: contract.chatroomId ? messageId : 0, - friendMessageId: contract.chatroomId ? 0 : messageId, - seq: `${+new Date()}`, // 使用时间戳作为请求序列号 - tencentUrl: tencentUrl, - wechatAccountId: contract.wechatAccountId, - }); - }; - - // 解析消息内容,判断消息类型并返回对应的渲染内容 - const parseMessageContent = ( - content: string | null | undefined, - msg: ChatRecord, - ) => { - // 处理null或undefined的内容 - if (content === null || content === undefined) { - return
消息内容不可用
; - } - // 检查是否为表情包 - if ( - typeof content === "string" && - content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") && - content.includes("#") - ) { - return ( -
- 表情包 window.open(content, "_blank")} - /> -
- ); - } - - // 检查是否为带预览图的视频消息 - try { - if ( - typeof content === "string" && - content.trim().startsWith("{") && - content.trim().endsWith("}") - ) { - const videoData = JSON.parse(content); - // 处理视频消息格式 {"previewImage":"https://...", "tencentUrl":"...", "videoUrl":"...", "isLoading":true} - if (videoData.previewImage && videoData.tencentUrl) { - // 提取预览图URL,去掉可能的引号 - const previewImageUrl = videoData.previewImage.replace(/[`"']/g, ""); - - // 创建点击处理函数 - const handlePlayClick = (e: React.MouseEvent) => { - e.stopPropagation(); - // 如果没有视频URL且不在加载中,则发起下载请求 - if (!videoData.videoUrl && !videoData.isLoading) { - handleVideoPlayRequest(videoData.tencentUrl, msg.id); - } - }; - - // 如果已有视频URL,显示视频播放器 - if (videoData.videoUrl) { - return ( -
- -
- ); - } - - // 显示预览图,根据加载状态显示不同的图标 - return ( -
-
- 视频预览 -
- {videoData.isLoading ? ( -
- ) : ( - - )} -
-
-
- ); - } - // 保留原有的视频处理逻辑 - else if ( - videoData.type === "video" && - videoData.url && - videoData.thumb - ) { - return ( -
-
window.open(videoData.url, "_blank")} - > - 视频预览 -
- -
-
- e.stopPropagation()} - > - - -
- ); - } - } - } catch (e) { - // 解析JSON失败,不是视频消息 - console.log("解析视频消息失败:", e); - } - - // 检查是否为图片链接 - if ( - typeof content === "string" && - (content.match(/\.(jpg|jpeg|png|gif)$/i) || - (content.includes("oss-cn-shenzhen.aliyuncs.com") && - content.includes(".jpg"))) - ) { - return ( -
- 图片消息 window.open(content, "_blank")} - /> -
- ); - } - - // 检查是否为视频链接 - if ( - typeof content === "string" && - (content.match(/\.(mp4|avi|mov|wmv|flv)$/i) || - (content.includes("oss-cn-shenzhen.aliyuncs.com") && - content.includes(".mp4"))) - ) { - return ( -
-
- ); - } - - // 检查是否为音频链接 - if ( - typeof content === "string" && - (content.match(/\.(mp3|wav|ogg|m4a)$/i) || - (content.includes("oss-cn-shenzhen.aliyuncs.com") && - content.includes(".mp3"))) - ) { - return ( -
-
- ); - } - - // 检查是否为Office文件链接 - if ( - typeof content === "string" && - content.match(/\.(doc|docx|xls|xlsx|ppt|pptx|pdf)$/i) - ) { - const fileName = content.split("/").pop() || "文件"; - const fileExt = fileName.split(".").pop()?.toLowerCase(); - - // 根据文件类型选择不同的图标 - let fileIcon = ( - - ); - - if (fileExt === "pdf") { - fileIcon = ( - - ); - } else if (fileExt === "doc" || fileExt === "docx") { - fileIcon = ( - - ); - } else if (fileExt === "xls" || fileExt === "xlsx") { - fileIcon = ( - - ); - } else if (fileExt === "ppt" || fileExt === "pptx") { - fileIcon = ( - - ); - } - - return ( - - ); - } - - // 检查是否为文件消息(JSON格式) - try { - if ( - typeof content === "string" && - content.trim().startsWith("{") && - content.trim().endsWith("}") - ) { - const fileData = JSON.parse(content); - if (fileData.type === "file" && fileData.title) { - // 检查是否为Office文件 - const fileExt = fileData.title.split(".").pop()?.toLowerCase(); - let fileIcon = ( - - ); - - if (fileExt === "pdf") { - fileIcon = ( - - ); - } else if (fileExt === "doc" || fileExt === "docx") { - fileIcon = ( - - ); - } else if (fileExt === "xls" || fileExt === "xlsx") { - fileIcon = ( - - ); - } else if (fileExt === "ppt" || fileExt === "pptx") { - fileIcon = ( - - ); - } - - return ( -
- {fileIcon} -
-
- {fileData.title} -
- {fileData.totalLen && ( -
- {Math.round(fileData.totalLen / 1024)} KB -
- )} -
- { - e.stopPropagation(); - if (!fileData.url) { - console.log("文件URL不存在"); - } - }} - rel="noreferrer" - > - - -
- ); - } - } - } catch (e) { - // 解析JSON失败,不是文件消息 - } - - // 检查是否为位置信息 - if ( - typeof content === "string" && - (content.includes(" - -
-
{label}
- {coordinates && ( -
- {coordinates} -
- )} -
-
- ); - } - - // 默认为文本消息 - return
{content}
; - }; - - // 用于分组消息并添加时间戳的辅助函数 - const groupMessagesByTime = (messages: ChatRecord[]) => { - return messages - .filter(msg => msg !== null && msg !== undefined) // 过滤掉null和undefined的消息 - .map(msg => ({ - time: formatWechatTime(msg?.wechatTime), - messages: [msg], - })); - }; - const groupMemberAvatar = (msg: ChatRecord) => { - const groupMembers = currentGroupMembers.find( - v => v.wechatId == msg.sender.wechatId, - ); - return groupMembers.avatar; - }; - const clearWechatidInContent = (sender, content: string) => { - return content.replace(new RegExp(`${sender.wechatId}:\n`, "g"), ""); - }; - const renderMessage = (msg: ChatRecord) => { - console.log(msg); - // 添加null检查,防止访问null对象的属性 - if (!msg) return null; - - const isOwn = msg?.isSend; - const isGroup = !!contract.chatroomId; - return ( -
-
- {/* 如果不是群聊 */} - {!isGroup && !isOwn && ( - <> - } - className={styles.messageAvatar} - /> - -
- {!isOwn && ( -
- {contract.nickname} -
- )} - {parseMessageContent(msg?.content, msg)} -
- - )} - {/* 如果是群聊 */} - {isGroup && !isOwn && ( - <> - } - className={styles.messageAvatar} - /> - -
- {!isOwn && ( -
- {msg?.sender?.nickname} -
- )} - {parseMessageContent( - clearWechatidInContent(msg?.sender, msg?.content), - msg, - )} -
- - )} - - {isOwn && ( -
- {parseMessageContent(msg?.content, msg)} -
- )} -
-
- ); - }; - const chatMenu = ( }> @@ -689,15 +109,12 @@ const ChatWindow: React.FC = ({ {/* 聊天内容 */} -
- {groupMessagesByTime(currentMessages).map((group, groupIndex) => ( - -
{group.time}
- {group.messages.map(renderMessage)} -
- ))} -
-
+ {/* 消息输入组件 */}