Merge branch 'yongpxu-dev' into develop
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import React, { useEffect, useRef } 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";
|
||||||
|
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
||||||
|
|
||||||
|
interface MessageRecordProps {
|
||||||
|
contract: ContractData | weChatGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const currentMessages = useWeChatStore(state => state.currentMessages);
|
||||||
|
const currentGroupMembers = useWeChatStore(
|
||||||
|
state => state.currentGroupMembers,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(currentMessages);
|
||||||
|
scrollToBottom();
|
||||||
|
}, [currentMessages]);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析消息内容,判断消息类型并返回对应的渲染内容
|
||||||
|
const parseMessageContent = (content: string | null | undefined) => {
|
||||||
|
// 处理null或undefined的内容
|
||||||
|
if (content === null || content === undefined) {
|
||||||
|
return <div className={styles.messageText}>消息内容不可用</div>;
|
||||||
|
}
|
||||||
|
// 检查是否为表情包
|
||||||
|
if (
|
||||||
|
typeof content === "string" &&
|
||||||
|
content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") &&
|
||||||
|
content.includes("#")
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className={styles.emojiMessage}>
|
||||||
|
<img
|
||||||
|
src={content}
|
||||||
|
alt="表情包"
|
||||||
|
style={{ maxWidth: "120px", maxHeight: "120px" }}
|
||||||
|
onClick={() => window.open(content, "_blank")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为带预览图的视频消息
|
||||||
|
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 (
|
||||||
|
<div className={styles.videoMessage}>
|
||||||
|
<div className={styles.videoContainer}>
|
||||||
|
<img
|
||||||
|
src={previewImageUrl}
|
||||||
|
alt="视频预览"
|
||||||
|
className={styles.videoPreview}
|
||||||
|
onClick={() => {
|
||||||
|
if (videoData.videoUrl) {
|
||||||
|
window.open(videoData.videoUrl, "_blank");
|
||||||
|
} else if (videoData.tencentUrl) {
|
||||||
|
window.open(videoData.tencentUrl, "_blank");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={styles.playButton}>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
|
||||||
|
<path d="M8 5v14l11-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// JSON解析失败,按普通文本处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通文本消息
|
||||||
|
return <div className={styles.messageText}>{content}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取群成员头像
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
key={msg.id || `msg-${Date.now()}`}
|
||||||
|
className={`${styles.messageItem} ${
|
||||||
|
isOwn ? styles.ownMessage : styles.otherMessage
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={styles.messageContent}>
|
||||||
|
{/* 如果不是群聊 */}
|
||||||
|
{!isGroup && !isOwn && (
|
||||||
|
<>
|
||||||
|
<Avatar
|
||||||
|
size={32}
|
||||||
|
src={contract.avatar}
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
className={styles.messageAvatar}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.messageBubble}>
|
||||||
|
{!isOwn && (
|
||||||
|
<div className={styles.messageSender}>
|
||||||
|
{contract.nickname}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{parseMessageContent(msg?.content)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* 如果是群聊 */}
|
||||||
|
{isGroup && !isOwn && (
|
||||||
|
<>
|
||||||
|
<Avatar
|
||||||
|
size={32}
|
||||||
|
src={groupMemberAvatar(msg)}
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
className={styles.messageAvatar}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.messageBubble}>
|
||||||
|
{!isOwn && (
|
||||||
|
<div className={styles.messageSender}>
|
||||||
|
{msg?.sender?.nickname}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{parseMessageContent(
|
||||||
|
clearWechatidInContent(msg?.sender, msg?.content),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOwn && (
|
||||||
|
<div className={styles.messageBubble}>
|
||||||
|
{parseMessageContent(msg?.content)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.messagesContainer}>
|
||||||
|
{groupMessagesByTime(currentMessages).map((group, groupIndex) => (
|
||||||
|
<React.Fragment key={`group-${groupIndex}`}>
|
||||||
|
<div className={styles.messageTime}>{group.time}</div>
|
||||||
|
{group.messages.map(renderMessage)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageRecord;
|
||||||
@@ -1,708 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import {
|
|
||||||
Layout,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Avatar,
|
|
||||||
Tooltip,
|
|
||||||
Card,
|
|
||||||
Tag,
|
|
||||||
message,
|
|
||||||
Modal,
|
|
||||||
} from "antd";
|
|
||||||
import {
|
|
||||||
PhoneOutlined,
|
|
||||||
VideoCameraOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
TeamOutlined,
|
|
||||||
MailOutlined,
|
|
||||||
EnvironmentOutlined,
|
|
||||||
CalendarOutlined,
|
|
||||||
BankOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
StarOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
CheckOutlined,
|
|
||||||
PlusOutlined,
|
|
||||||
NotificationOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
|
||||||
import { useCkChatStore } from "@/store/module/ckchat/ckchat";
|
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
|
||||||
import styles from "./Person.module.scss";
|
|
||||||
|
|
||||||
const { Sider } = Layout;
|
|
||||||
|
|
||||||
interface PersonProps {
|
|
||||||
contract: ContractData | weChatGroup;
|
|
||||||
showProfile: boolean;
|
|
||||||
onToggleProfile?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Person: React.FC<PersonProps> = ({
|
|
||||||
contract,
|
|
||||||
showProfile,
|
|
||||||
onToggleProfile,
|
|
||||||
}) => {
|
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
|
||||||
const [isEditingRemark, setIsEditingRemark] = useState(false);
|
|
||||||
const [remarkValue, setRemarkValue] = useState(contract.conRemark || "");
|
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>(
|
|
||||||
contract.labels || [],
|
|
||||||
);
|
|
||||||
const [allAvailableTags, setAllAvailableTags] = useState<string[]>([]);
|
|
||||||
const [isAddingTag, setIsAddingTag] = useState(false);
|
|
||||||
const [newTagValue, setNewTagValue] = useState("");
|
|
||||||
|
|
||||||
// 判断是否为群聊
|
|
||||||
const isGroup = 'chatroomId' in contract;
|
|
||||||
|
|
||||||
// 群聊相关状态
|
|
||||||
const [isEditingGroupName, setIsEditingGroupName] = useState(false);
|
|
||||||
const [groupNameValue, setGroupNameValue] = useState(contract.name || '');
|
|
||||||
const [isEditingGroupNotice, setIsEditingGroupNotice] = useState(false);
|
|
||||||
const [groupNoticeValue, setGroupNoticeValue] = useState(contract.notice || '');
|
|
||||||
const [isEditingSelfDisplayName, setIsEditingSelfDisplayName] = useState(false);
|
|
||||||
const [selfDisplayNameValue, setSelfDisplayNameValue] = useState(contract.selfDisplyName || '');
|
|
||||||
const [isGroupNoticeModalVisible, setIsGroupNoticeModalVisible] = useState(false);
|
|
||||||
|
|
||||||
// 构建联系人或群聊详细信息
|
|
||||||
|
|
||||||
const kfSelectedUser = useCkChatStore(state =>
|
|
||||||
state.getKfUserInfo(contract.wechatAccountId || 0),
|
|
||||||
);
|
|
||||||
const { sendCommand } = useWebSocketStore();
|
|
||||||
|
|
||||||
// 获取所有可用标签
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchAvailableTags = async () => {
|
|
||||||
try {
|
|
||||||
// 从kfSelectedUser.labels和contract.labels合并获取所有标签
|
|
||||||
const kfTags = kfSelectedUser?.labels || [];
|
|
||||||
const contractTags = contract.labels || [];
|
|
||||||
const allTags = [...new Set([...kfTags, ...contractTags])];
|
|
||||||
setAllAvailableTags(allTags);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("获取标签失败:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchAvailableTags();
|
|
||||||
}, [kfSelectedUser, contract.labels]);
|
|
||||||
|
|
||||||
// 当contract变化时更新各种值
|
|
||||||
useEffect(() => {
|
|
||||||
setRemarkValue(contract.conRemark || "");
|
|
||||||
setIsEditingRemark(false);
|
|
||||||
setSelectedTags(contract.labels || []);
|
|
||||||
|
|
||||||
if (isGroup) {
|
|
||||||
setGroupNameValue(contract.name || '');
|
|
||||||
setIsEditingGroupName(false);
|
|
||||||
setGroupNoticeValue(contract.notice || '');
|
|
||||||
setIsEditingGroupNotice(false);
|
|
||||||
setSelfDisplayNameValue(contract.selfDisplyName || '');
|
|
||||||
setIsEditingSelfDisplayName(false);
|
|
||||||
}
|
|
||||||
}, [contract.conRemark, contract.labels, contract.name, contract.notice, contract.selfDisplyName, isGroup]);
|
|
||||||
|
|
||||||
// 处理备注保存
|
|
||||||
const handleSaveRemark = () => {
|
|
||||||
if (isGroup) {
|
|
||||||
// 群聊备注修改
|
|
||||||
sendCommand("CmdModifyGroupRemark", {
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
chatroomId: contract.chatroomId,
|
|
||||||
newRemark: remarkValue,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 好友备注修改
|
|
||||||
sendCommand("CmdModifyFriendRemark", {
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
wechatFriendId: contract.id,
|
|
||||||
newRemark: remarkValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
messageApi.success("备注保存成功");
|
|
||||||
setIsEditingRemark(false);
|
|
||||||
// 更新contract对象中的备注(实际项目中应该通过props回调或状态管理)
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理群名称保存
|
|
||||||
const handleSaveGroupName = () => {
|
|
||||||
sendCommand("CmdChatroomOperate", {
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
wechatChatroomId: contract.id,
|
|
||||||
chatroomOperateType: 6,
|
|
||||||
extra: `{\"chatroomName\":\"${groupNameValue}\"}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
messageApi.success("群名称修改成功");
|
|
||||||
setIsEditingGroupName(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 点击编辑群名称按钮
|
|
||||||
const handleEditGroupName = () => {
|
|
||||||
setGroupNameValue(contract.name || '');
|
|
||||||
setIsEditingGroupName(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理群公告保存
|
|
||||||
const handleSaveGroupNotice = () => {
|
|
||||||
sendCommand("CmdChatroomOperate", {
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
wechatChatroomId: contract.id,
|
|
||||||
chatroomOperateType: 5,
|
|
||||||
extra: `{\"announce\":\"${groupNoticeValue}\"}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
messageApi.success("群公告修改成功");
|
|
||||||
setIsEditingGroupNotice(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 点击编辑群公告按钮
|
|
||||||
const handleEditGroupNotice = () => {
|
|
||||||
setGroupNoticeValue(contract.notice || '');
|
|
||||||
setIsGroupNoticeModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理我在本群中的昵称保存
|
|
||||||
const handleSaveSelfDisplayName = () => {
|
|
||||||
sendCommand("CmdChatroomOperate", {
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
wechatChatroomId: contract.id,
|
|
||||||
chatroomOperateType: 8,
|
|
||||||
extra: `${selfDisplayNameValue}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
messageApi.success("群昵称修改成功");
|
|
||||||
setIsEditingSelfDisplayName(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 点击编辑群昵称按钮
|
|
||||||
const handleEditSelfDisplayName = () => {
|
|
||||||
setSelfDisplayNameValue(contract.selfDisplyName || '');
|
|
||||||
setIsEditingSelfDisplayName(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理取消编辑
|
|
||||||
const handleCancelEdit = () => {
|
|
||||||
setRemarkValue(contract.conRemark || "");
|
|
||||||
setIsEditingRemark(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理标签点击切换
|
|
||||||
const handleTagToggle = (tagName: string) => {
|
|
||||||
const newSelectedTags = selectedTags.includes(tagName)
|
|
||||||
? selectedTags.filter(tag => tag !== tagName)
|
|
||||||
: [...selectedTags, tagName];
|
|
||||||
|
|
||||||
setSelectedTags(newSelectedTags);
|
|
||||||
|
|
||||||
// 使用WebSocket发送修改标签命令
|
|
||||||
if (isGroup) {
|
|
||||||
// 群聊标签修改
|
|
||||||
sendCommand("CmdModifyGroupLabel", {
|
|
||||||
labels: newSelectedTags,
|
|
||||||
seq: +new Date(),
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
chatroomId: contract.chatroomId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 好友标签修改
|
|
||||||
sendCommand("CmdModifyFriendLabel", {
|
|
||||||
labels: newSelectedTags,
|
|
||||||
seq: +new Date(),
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
wechatFriendId: contract.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
messageApi.success(
|
|
||||||
`标签"${tagName}"${selectedTags.includes(tagName) ? "已取消" : "已选中"}`,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理新增标签
|
|
||||||
const handleAddTag = () => {
|
|
||||||
if (!newTagValue.trim()) {
|
|
||||||
messageApi.error("请输入标签名称");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allAvailableTags.includes(newTagValue.trim())) {
|
|
||||||
messageApi.error("标签已存在");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newTag = newTagValue.trim();
|
|
||||||
|
|
||||||
// 添加到可用标签列表
|
|
||||||
setAllAvailableTags(prev => [...prev, newTag]);
|
|
||||||
|
|
||||||
// 自动选中新添加的标签
|
|
||||||
setSelectedTags(prev => [...prev, newTag]);
|
|
||||||
|
|
||||||
// 使用WebSocket发送新增标签命令
|
|
||||||
if (isGroup) {
|
|
||||||
// 群聊标签修改
|
|
||||||
sendCommand("CmdModifyGroupLabel", {
|
|
||||||
labels: [...selectedTags, newTag],
|
|
||||||
seq: +new Date(),
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
chatroomId: contract.chatroomId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 好友标签修改
|
|
||||||
sendCommand("CmdModifyFriendLabel", {
|
|
||||||
labels: [...selectedTags, newTag],
|
|
||||||
seq: +new Date(),
|
|
||||||
wechatAccountId: contract.wechatAccountId,
|
|
||||||
wechatFriendId: contract.id || contract.wechatId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
messageApi.success(`标签"${newTag}"添加成功`);
|
|
||||||
setNewTagValue("");
|
|
||||||
setIsAddingTag(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理取消新增标签
|
|
||||||
const handleCancelAddTag = () => {
|
|
||||||
setNewTagValue("");
|
|
||||||
setIsAddingTag(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 构建联系人或群聊详细信息
|
|
||||||
const contractInfo = {
|
|
||||||
name: contract.name || contract.nickname,
|
|
||||||
nickname: contract.nickname,
|
|
||||||
conRemark: remarkValue, // 使用当前编辑的备注值
|
|
||||||
alias: contract.alias,
|
|
||||||
wechatId: contract.wechatId,
|
|
||||||
chatroomId: isGroup ? contract.chatroomId : undefined,
|
|
||||||
chatroomOwner: isGroup ? contract.chatroomOwner : undefined,
|
|
||||||
avatar: contract.avatar || contract.chatroomAvatar,
|
|
||||||
phone: contract.phone || "-",
|
|
||||||
email: contract.email || "-",
|
|
||||||
department: contract.department || "-",
|
|
||||||
position: contract.position || "-",
|
|
||||||
company: contract.company || "-",
|
|
||||||
region: contract.region || "-",
|
|
||||||
joinDate: contract.joinDate || "-",
|
|
||||||
notice: isGroup ? contract.notice : undefined,
|
|
||||||
selfDisplyName: isGroup ? contract.selfDisplyName : undefined,
|
|
||||||
status: "在线",
|
|
||||||
tags: selectedTags,
|
|
||||||
bio: contract.bio || contract.signature || "-",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!showProfile) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{contextHolder}
|
|
||||||
<Sider width={320} className={styles.profileSider}>
|
|
||||||
<div className={styles.profileContainer}>
|
|
||||||
{/* 关闭按钮 */}
|
|
||||||
<div className={styles.profileHeader}>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={onToggleProfile}
|
|
||||||
className={styles.closeButton}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 头像和基本信息 */}
|
|
||||||
<div className={styles.profileBasic}>
|
|
||||||
<Avatar
|
|
||||||
size={80}
|
|
||||||
src={contractInfo.avatar}
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
/>
|
|
||||||
<div className={styles.profileInfo}>
|
|
||||||
{isGroup && isEditingGroupName ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={groupNameValue}
|
|
||||||
onChange={e => setGroupNameValue(e.target.value)}
|
|
||||||
placeholder="请输入群名称"
|
|
||||||
size="small"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CheckOutlined />}
|
|
||||||
onClick={handleSaveGroupName}
|
|
||||||
style={{ color: "#52c41a" }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
setGroupNameValue(contract.name || '');
|
|
||||||
setIsEditingGroupName(false);
|
|
||||||
}}
|
|
||||||
style={{ color: "#ff4d4f" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Tooltip
|
|
||||||
title={contractInfo.nickname || contractInfo.name}
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<h4 className={styles.profileNickname}>
|
|
||||||
{contractInfo.nickname || contractInfo.name}
|
|
||||||
</h4>
|
|
||||||
{isGroup && (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => setIsEditingGroupName(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={styles.profileStatus}>
|
|
||||||
<span className={styles.statusDot}></span>
|
|
||||||
{contractInfo.status}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 详细信息卡片 */}
|
|
||||||
<Card title="详细信息" className={styles.profileCard}>
|
|
||||||
{isGroup ? (
|
|
||||||
// 群聊信息
|
|
||||||
<>
|
|
||||||
<div className={styles.infoItem}>
|
|
||||||
<TeamOutlined className={styles.infoIcon} />
|
|
||||||
<span className={styles.infoLabel}>群ID:</span>
|
|
||||||
<span className={styles.infoValue}>
|
|
||||||
{contractInfo.chatroomId}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.infoItem}>
|
|
||||||
<UserOutlined className={styles.infoIcon} />
|
|
||||||
<span className={styles.infoLabel}>群主:</span>
|
|
||||||
<span className={styles.infoValue}>{contractInfo.chatroomOwner}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.infoItem}>
|
|
||||||
<UserOutlined className={styles.infoIcon} />
|
|
||||||
<span className={styles.infoLabel}>群昵称:</span>
|
|
||||||
<div className={styles.infoValue}>
|
|
||||||
{isEditingSelfDisplayName ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={selfDisplayNameValue}
|
|
||||||
onChange={e => setSelfDisplayNameValue(e.target.value)}
|
|
||||||
placeholder="请输入群昵称"
|
|
||||||
size="small"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CheckOutlined />}
|
|
||||||
onClick={handleSaveSelfDisplayName}
|
|
||||||
style={{ color: "#52c41a" }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
setSelfDisplayNameValue(contract.selfDisplyName || '');
|
|
||||||
setIsEditingSelfDisplayName(false);
|
|
||||||
}}
|
|
||||||
style={{ color: "#ff4d4f" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{contractInfo.selfDisplyName || "点击添加群昵称"}</span>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => setIsEditingSelfDisplayName(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// 好友信息
|
|
||||||
<>
|
|
||||||
<div className={styles.infoItem}>
|
|
||||||
<TeamOutlined className={styles.infoIcon} />
|
|
||||||
<span className={styles.infoLabel}>微信号:</span>
|
|
||||||
<span className={styles.infoValue}>
|
|
||||||
{contractInfo.alias || contractInfo.wechatId}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.infoItem}>
|
|
||||||
<PhoneOutlined className={styles.infoIcon} />
|
|
||||||
<span className={styles.infoLabel}>电话:</span>
|
|
||||||
<span className={styles.infoValue}>{contractInfo.phone}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.infoItem}>
|
|
||||||
<EnvironmentOutlined className={styles.infoIcon} />
|
|
||||||
<span className={styles.infoLabel}>地区:</span>
|
|
||||||
<span className={styles.infoValue}>{contractInfo.region}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isGroup && (
|
|
||||||
<div className={styles.infoItem}>
|
|
||||||
<EditOutlined className={styles.infoIcon} />
|
|
||||||
<span className={styles.infoLabel}>备注:</span>
|
|
||||||
<div className={styles.infoValue}>
|
|
||||||
{isEditingRemark ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={remarkValue}
|
|
||||||
onChange={e => setRemarkValue(e.target.value)}
|
|
||||||
placeholder="请输入备注"
|
|
||||||
size="small"
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CheckOutlined />}
|
|
||||||
onClick={handleSaveRemark}
|
|
||||||
style={{ color: "#52c41a" }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={handleCancelEdit}
|
|
||||||
style={{ color: "#ff4d4f" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{contractInfo.conRemark || "点击添加备注"}</span>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={() => setIsEditingRemark(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 标签 - 仅在非群聊时显示 */}
|
|
||||||
{!isGroup && (
|
|
||||||
<Card title="标签" className={styles.profileCard}>
|
|
||||||
<div className={styles.tagsContainer}>
|
|
||||||
{/* 渲染所有可用标签,选中的排在前面 */}
|
|
||||||
{[...new Set([...selectedTags, ...allAvailableTags])].map(
|
|
||||||
(tag, index) => {
|
|
||||||
const isSelected = selectedTags.includes(tag);
|
|
||||||
return (
|
|
||||||
<Tag
|
|
||||||
key={index}
|
|
||||||
color={isSelected ? "blue" : "default"}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
border: isSelected
|
|
||||||
? "1px solid #1890ff"
|
|
||||||
: "1px solid #d9d9d9",
|
|
||||||
backgroundColor: isSelected ? "#e6f7ff" : "#fafafa",
|
|
||||||
}}
|
|
||||||
onClick={() => handleTagToggle(tag)}
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 新增标签区域 */}
|
|
||||||
{isAddingTag ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "8px",
|
|
||||||
marginTop: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={newTagValue}
|
|
||||||
onChange={e => setNewTagValue(e.target.value)}
|
|
||||||
placeholder="请输入标签名称"
|
|
||||||
size="small"
|
|
||||||
style={{ width: "120px" }}
|
|
||||||
onPressEnter={handleAddTag}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CheckOutlined />}
|
|
||||||
onClick={handleAddTag}
|
|
||||||
style={{ color: "#52c41a" }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CloseOutlined />}
|
|
||||||
onClick={handleCancelAddTag}
|
|
||||||
style={{ color: "#ff4d4f" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Tag
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
border: "1px dashed #d9d9d9",
|
|
||||||
backgroundColor: "#fafafa",
|
|
||||||
}}
|
|
||||||
onClick={() => setIsAddingTag(true)}
|
|
||||||
>
|
|
||||||
<PlusOutlined /> 新增标签
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allAvailableTags.length === 0 && !isAddingTag && (
|
|
||||||
<span style={{ color: "#999", fontSize: "12px" }}>
|
|
||||||
暂无可用标签
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 个人简介或群公告 */}
|
|
||||||
<Card title={isGroup ? "群公告" : "个人简介"} className={styles.profileCard}>
|
|
||||||
{isGroup ? (
|
|
||||||
// 群聊简介(原群公告)
|
|
||||||
<div className={styles.infoValue}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
gap: "8px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={styles.bioText}
|
|
||||||
style={{
|
|
||||||
maxHeight: "120px", // 约5行文本的高度
|
|
||||||
overflowY: "auto", // 添加垂直滚动条
|
|
||||||
paddingRight: "5px", // 为滚动条留出空间
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{contractInfo.notice || "点击添加群公告"}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<EditOutlined />}
|
|
||||||
onClick={handleEditGroupNotice}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// 个人简介
|
|
||||||
<p className={styles.bioText}>{contractInfo.bio}</p>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<div className={styles.profileActions}>
|
|
||||||
<Button type="primary" icon={<PhoneOutlined />} block>
|
|
||||||
语音通话
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
icon={<VideoCameraOutlined />}
|
|
||||||
block
|
|
||||||
style={{ marginTop: 8 }}
|
|
||||||
>
|
|
||||||
视频通话
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Sider>
|
|
||||||
|
|
||||||
{/* 群公告编辑弹窗 */}
|
|
||||||
<Modal
|
|
||||||
title="发布群公告"
|
|
||||||
open={isGroupNoticeModalVisible}
|
|
||||||
onCancel={() => setIsGroupNoticeModalVisible(false)}
|
|
||||||
footer={[
|
|
||||||
<Button key="cancel" onClick={() => setIsGroupNoticeModalVisible(false)}>
|
|
||||||
取消
|
|
||||||
</Button>,
|
|
||||||
<Button
|
|
||||||
key="submit"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
handleSaveGroupNotice();
|
|
||||||
setIsGroupNoticeModalVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input.TextArea
|
|
||||||
value={groupNoticeValue}
|
|
||||||
onChange={e => setGroupNoticeValue(e.target.value)}
|
|
||||||
placeholder="请输入内容"
|
|
||||||
rows={6}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Person;
|
|
||||||
@@ -5,24 +5,15 @@ import {
|
|||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
DownloadOutlined,
|
|
||||||
FileOutlined,
|
|
||||||
FilePdfOutlined,
|
|
||||||
FileWordOutlined,
|
|
||||||
FileExcelOutlined,
|
|
||||||
FilePptOutlined,
|
|
||||||
PlayCircleFilled,
|
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
FolderOutlined,
|
|
||||||
EnvironmentOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
import { ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||||
import styles from "./ChatWindow.module.scss";
|
import styles from "./ChatWindow.module.scss";
|
||||||
import { useWebSocketStore } from "@/store/module/websocket/websocket";
|
|
||||||
import { formatWechatTime } from "@/utils/common";
|
|
||||||
import ProfileCard from "./components/ProfileCard";
|
import ProfileCard from "./components/ProfileCard";
|
||||||
import MessageEnter from "./components/MessageEnter";
|
import MessageEnter from "./components/MessageEnter";
|
||||||
import { useWeChatStore } from "@/store/module/weChat/weChat";
|
import MessageRecord from "./components/MessageRecord";
|
||||||
|
|
||||||
const { Header, Content } = Layout;
|
const { Header, Content } = Layout;
|
||||||
|
|
||||||
interface ChatWindowProps {
|
interface ChatWindowProps {
|
||||||
@@ -36,591 +27,6 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
showProfile = true,
|
showProfile = true,
|
||||||
onToggleProfile,
|
onToggleProfile,
|
||||||
}) => {
|
}) => {
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
||||||
const currentMessages = useWeChatStore(state => state.currentMessages);
|
|
||||||
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;
|
|
||||||
}, [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 <div className={styles.messageText}>消息内容不可用</div>;
|
|
||||||
}
|
|
||||||
// 检查是否为表情包
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
content.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") &&
|
|
||||||
content.includes("#")
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.emojiMessage}>
|
|
||||||
<img
|
|
||||||
src={content}
|
|
||||||
alt="表情包"
|
|
||||||
style={{ maxWidth: "120px", maxHeight: "120px" }}
|
|
||||||
onClick={() => window.open(content, "_blank")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为带预览图的视频消息
|
|
||||||
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 (
|
|
||||||
<div className={styles.videoMessage}>
|
|
||||||
<div className={styles.videoContainer}>
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
src={videoData.videoUrl}
|
|
||||||
style={{ maxWidth: "100%", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href={videoData.videoUrl}
|
|
||||||
download
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示预览图,根据加载状态显示不同的图标
|
|
||||||
return (
|
|
||||||
<div className={styles.videoMessage}>
|
|
||||||
<div className={styles.videoContainer} onClick={handlePlayClick}>
|
|
||||||
<img
|
|
||||||
src={previewImageUrl}
|
|
||||||
alt="视频预览"
|
|
||||||
className={styles.videoThumbnail}
|
|
||||||
style={{
|
|
||||||
maxWidth: "100%",
|
|
||||||
borderRadius: "8px",
|
|
||||||
opacity: videoData.isLoading ? "0.7" : "1",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className={styles.videoPlayIcon}>
|
|
||||||
{videoData.isLoading ? (
|
|
||||||
<div className={styles.loadingSpinner}></div>
|
|
||||||
) : (
|
|
||||||
<PlayCircleFilled
|
|
||||||
style={{ fontSize: "48px", color: "#fff" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 保留原有的视频处理逻辑
|
|
||||||
else if (
|
|
||||||
videoData.type === "video" &&
|
|
||||||
videoData.url &&
|
|
||||||
videoData.thumb
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.videoMessage}>
|
|
||||||
<div
|
|
||||||
className={styles.videoContainer}
|
|
||||||
onClick={() => window.open(videoData.url, "_blank")}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={videoData.thumb}
|
|
||||||
alt="视频预览"
|
|
||||||
className={styles.videoThumbnail}
|
|
||||||
/>
|
|
||||||
<div className={styles.videoPlayIcon}>
|
|
||||||
<VideoCameraOutlined
|
|
||||||
style={{ fontSize: "32px", color: "#fff" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={videoData.url}
|
|
||||||
download
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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 (
|
|
||||||
<div className={styles.imageMessage}>
|
|
||||||
<img
|
|
||||||
src={content}
|
|
||||||
alt="图片消息"
|
|
||||||
onClick={() => window.open(content, "_blank")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为视频链接
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
(content.match(/\.(mp4|avi|mov|wmv|flv)$/i) ||
|
|
||||||
(content.includes("oss-cn-shenzhen.aliyuncs.com") &&
|
|
||||||
content.includes(".mp4")))
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.videoMessage}>
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
src={content}
|
|
||||||
style={{ maxWidth: "100%", borderRadius: "8px" }}
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href={content}
|
|
||||||
download
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为音频链接
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
(content.match(/\.(mp3|wav|ogg|m4a)$/i) ||
|
|
||||||
(content.includes("oss-cn-shenzhen.aliyuncs.com") &&
|
|
||||||
content.includes(".mp3")))
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<div className={styles.audioMessage}>
|
|
||||||
<audio controls src={content} style={{ maxWidth: "100%" }} />
|
|
||||||
<a
|
|
||||||
href={content}
|
|
||||||
download
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为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 = (
|
|
||||||
<FileOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#1890ff" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fileExt === "pdf") {
|
|
||||||
fileIcon = (
|
|
||||||
<FilePdfOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#ff4d4f" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "doc" || fileExt === "docx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FileWordOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#2f54eb" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "xls" || fileExt === "xlsx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FileExcelOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#52c41a" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "ppt" || fileExt === "pptx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FilePptOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#fa8c16" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.fileMessage}>
|
|
||||||
{fileIcon}
|
|
||||||
<div className={styles.fileInfo}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fileName}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={content}
|
|
||||||
download={fileExt !== "pdf" ? fileName : undefined}
|
|
||||||
target={fileExt === "pdf" ? "_blank" : undefined}
|
|
||||||
className={styles.downloadButton}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为文件消息(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 = (
|
|
||||||
<FolderOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#1890ff" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fileExt === "pdf") {
|
|
||||||
fileIcon = (
|
|
||||||
<FilePdfOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: "24px",
|
|
||||||
marginRight: "8px",
|
|
||||||
color: "#ff4d4f",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "doc" || fileExt === "docx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FileWordOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: "24px",
|
|
||||||
marginRight: "8px",
|
|
||||||
color: "#2f54eb",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "xls" || fileExt === "xlsx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FileExcelOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: "24px",
|
|
||||||
marginRight: "8px",
|
|
||||||
color: "#52c41a",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (fileExt === "ppt" || fileExt === "pptx") {
|
|
||||||
fileIcon = (
|
|
||||||
<FilePptOutlined
|
|
||||||
style={{
|
|
||||||
fontSize: "24px",
|
|
||||||
marginRight: "8px",
|
|
||||||
color: "#fa8c16",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.fileMessage}>
|
|
||||||
{fileIcon}
|
|
||||||
<div className={styles.fileInfo}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{fileData.title}
|
|
||||||
</div>
|
|
||||||
{fileData.totalLen && (
|
|
||||||
<div style={{ fontSize: "12px", color: "#8c8c8c" }}>
|
|
||||||
{Math.round(fileData.totalLen / 1024)} KB
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={fileData.url || "#"}
|
|
||||||
download={fileExt !== "pdf" ? fileData.title : undefined}
|
|
||||||
target={fileExt === "pdf" ? "_blank" : undefined}
|
|
||||||
className={styles.downloadButton}
|
|
||||||
style={{ display: "flex" }}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!fileData.url) {
|
|
||||||
console.log("文件URL不存在");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<DownloadOutlined style={{ fontSize: "18px" }} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 解析JSON失败,不是文件消息
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为位置信息
|
|
||||||
if (
|
|
||||||
typeof content === "string" &&
|
|
||||||
(content.includes("<location") || content.includes("<msg><location"))
|
|
||||||
) {
|
|
||||||
// 提取位置信息
|
|
||||||
const labelMatch = content.match(/label="([^"]*)"/i);
|
|
||||||
const poiNameMatch = content.match(/poiname="([^"]*)"/i);
|
|
||||||
const xMatch = content.match(/x="([^"]*)"/i);
|
|
||||||
const yMatch = content.match(/y="([^"]*)"/i);
|
|
||||||
|
|
||||||
const label = labelMatch
|
|
||||||
? labelMatch[1]
|
|
||||||
: poiNameMatch
|
|
||||||
? poiNameMatch[1]
|
|
||||||
: "位置信息";
|
|
||||||
const coordinates = xMatch && yMatch ? `${yMatch[1]}, ${xMatch[1]}` : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.locationMessage}>
|
|
||||||
<EnvironmentOutlined
|
|
||||||
style={{ fontSize: "24px", marginRight: "8px", color: "#ff4d4f" }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: "bold" }}>{label}</div>
|
|
||||||
{coordinates && (
|
|
||||||
<div style={{ fontSize: "12px", color: "#8c8c8c" }}>
|
|
||||||
{coordinates}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认为文本消息
|
|
||||||
return <div className={styles.messageText}>{content}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 用于分组消息并添加时间戳的辅助函数
|
|
||||||
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 (
|
|
||||||
<div
|
|
||||||
key={msg.id || `msg-${Date.now()}`}
|
|
||||||
className={`${styles.messageItem} ${
|
|
||||||
isOwn ? styles.ownMessage : styles.otherMessage
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className={styles.messageContent}>
|
|
||||||
{/* 如果不是群聊 */}
|
|
||||||
{!isGroup && !isOwn && (
|
|
||||||
<>
|
|
||||||
<Avatar
|
|
||||||
size={32}
|
|
||||||
src={contract.avatar}
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
className={styles.messageAvatar}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.messageBubble}>
|
|
||||||
{!isOwn && (
|
|
||||||
<div className={styles.messageSender}>
|
|
||||||
{contract.nickname}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{parseMessageContent(msg?.content, msg)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{/* 如果是群聊 */}
|
|
||||||
{isGroup && !isOwn && (
|
|
||||||
<>
|
|
||||||
<Avatar
|
|
||||||
size={32}
|
|
||||||
src={groupMemberAvatar(msg)}
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
className={styles.messageAvatar}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.messageBubble}>
|
|
||||||
{!isOwn && (
|
|
||||||
<div className={styles.messageSender}>
|
|
||||||
{msg?.sender?.nickname}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{parseMessageContent(
|
|
||||||
clearWechatidInContent(msg?.sender, msg?.content),
|
|
||||||
msg,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isOwn && (
|
|
||||||
<div className={styles.messageBubble}>
|
|
||||||
{parseMessageContent(msg?.content, msg)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const chatMenu = (
|
const chatMenu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Item key="profile" icon={<UserOutlined />}>
|
<Menu.Item key="profile" icon={<UserOutlined />}>
|
||||||
@@ -689,15 +95,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
|
|
||||||
{/* 聊天内容 */}
|
{/* 聊天内容 */}
|
||||||
<Content className={styles.chatContent}>
|
<Content className={styles.chatContent}>
|
||||||
<div className={styles.messagesContainer}>
|
<MessageRecord contract={contract} />
|
||||||
{groupMessagesByTime(currentMessages).map((group, groupIndex) => (
|
|
||||||
<React.Fragment key={`group-${groupIndex}`}>
|
|
||||||
<div className={styles.messageTime}>{group.time}</div>
|
|
||||||
{group.messages.map(renderMessage)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
{/* 消息输入组件 */}
|
{/* 消息输入组件 */}
|
||||||
|
|||||||
@@ -89,20 +89,27 @@ export const useWeChatStore = create<WeChatState>()(
|
|||||||
|
|
||||||
receivedMsg: message => {
|
receivedMsg: message => {
|
||||||
const currentContract = useWeChatStore.getState().currentContract;
|
const currentContract = useWeChatStore.getState().currentContract;
|
||||||
|
//判断群还是好友
|
||||||
|
const getMessageId =
|
||||||
|
currentContract?.chatroomId || message.wechatFriendId;
|
||||||
|
const isGroup = currentContract?.chatroomId;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
currentContract &&
|
currentContract &&
|
||||||
currentContract.wechatAccountId == message.wechatAccountId &&
|
currentContract.wechatAccountId == message.wechatAccountId &&
|
||||||
currentContract.id == message.wechatFriendId
|
currentContract.id == getMessageId
|
||||||
) {
|
) {
|
||||||
|
console.log("进入");
|
||||||
|
if (isGroup) {
|
||||||
|
message.unreadCount = 1;
|
||||||
|
}
|
||||||
set(state => ({
|
set(state => ({
|
||||||
currentMessages: [...state.currentMessages, message],
|
currentMessages: [...state.currentMessages, message],
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
//更新消息列表unread数值,根据接收的++1 这样
|
//更新消息列表unread数值,根据接收的++1 这样
|
||||||
const chatSessions = useCkChatStore.getState().chatSessions;
|
const chatSessions = useCkChatStore.getState().chatSessions;
|
||||||
const session = chatSessions.find(
|
const session = chatSessions.find(item => item.id == getMessageId);
|
||||||
item => item.id == message.wechatFriendId,
|
|
||||||
);
|
|
||||||
if (session) {
|
if (session) {
|
||||||
session.unreadCount = Number(session.unreadCount) + 1;
|
session.unreadCount = Number(session.unreadCount) + 1;
|
||||||
updateChatSession(session);
|
updateChatSession(session);
|
||||||
|
|||||||
Reference in New Issue
Block a user