Files
cunkebao_v3/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/index.tsx
超级老白兔 bdc94d853d refactor(组件): 优化微信好友列表和聊天窗口菜单代码结构
重构微信好友列表组件中的getCollapseItems方法,简化代码格式
将聊天窗口的菜单从JSX改为items数组配置方式,提高可维护性
2025-08-29 10:49:13 +08:00

1217 lines
37 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from "react";
import {
Layout,
Input,
Button,
Avatar,
Space,
Dropdown,
Menu,
message,
Tooltip,
Badge,
Card,
Tag,
Modal,
} from "antd";
import {
ShareAltOutlined,
SendOutlined,
SmileOutlined,
FolderOutlined,
PhoneOutlined,
VideoCameraOutlined,
MoreOutlined,
UserOutlined,
TeamOutlined,
MailOutlined,
EnvironmentOutlined,
CalendarOutlined,
BankOutlined,
CloseOutlined,
StarOutlined,
EnvironmentOutlined as LocationOutlined,
AudioOutlined,
AudioOutlined as AudioHoldOutlined,
DownloadOutlined,
CodeSandboxOutlined,
MessageOutlined,
FileOutlined,
FilePdfOutlined,
FileWordOutlined,
FileExcelOutlined,
FilePptOutlined,
PlayCircleFilled,
} from "@ant-design/icons";
import { ChatRecord, ContractData } from "@/pages/pc/ckbox/data";
import { clearUnreadCount, getMessages } from "@/pages/pc/ckbox/api";
import styles from "./ChatWindow.module.scss";
import { useWebSocketStore, WebSocketMessage } from "@/store/module/websocket";
import { formatWechatTime } from "@/utils/common";
const { Header, Content, Footer, Sider } = Layout;
const { TextArea } = Input;
interface ChatWindowProps {
contract: ContractData;
onSendMessage: (message: string) => void;
showProfile?: boolean;
onToggleProfile?: () => void;
}
const ChatWindow: React.FC<ChatWindowProps> = ({
contract,
onSendMessage,
showProfile = true,
onToggleProfile,
}) => {
const [messageApi, contextHolder] = message.useMessage();
const [messages, setMessages] = useState<ChatRecord[]>([]);
const [inputValue, setInputValue] = useState("");
const [loading, setLoading] = useState(false);
const [showMaterialModal, setShowMaterialModal] = useState(false);
const [pendingVideoRequests, setPendingVideoRequests] = useState<
Record<string, string>
>({});
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
clearUnreadCount([contract.id]).then(() => {
setLoading(true);
getMessages({
wechatAccountId: contract.wechatAccountId,
wechatFriendId: contract.id,
From: 1,
To: +new Date() + 1000,
Count: 100,
olderData: true,
})
.then(msg => {
setMessages(msg);
})
.finally(() => {
setLoading(false);
});
});
}, [contract.id]);
useEffect(() => {
// 只有在非视频加载操作时才自动滚动到底部
// 检查是否有视频正在加载中
const hasLoadingVideo = messages.some(msg => {
try {
const content =
typeof msg.content === "string"
? JSON.parse(msg.content)
: msg.content;
return content.isLoading === true;
} catch (e) {
return false;
}
});
if (!hasLoadingVideo) {
scrollToBottom();
}
}, [messages]);
// 添加 WebSocket 消息订阅 - 监听视频下载响应消息
useEffect(() => {
// 只有当有待处理的视频请求时才订阅WebSocket消息
if (Object.keys(pendingVideoRequests).length === 0) {
return;
}
console.log("开始监听视频下载响应,当前待处理请求:", pendingVideoRequests);
// 订阅 WebSocket 消息变化
const unsubscribe = useWebSocketStore.subscribe(state => {
// 只处理新增的消息
const messages = state.messages as WebSocketMessage[];
// 筛选出视频下载响应消息
messages.forEach(message => {
if (message?.content?.cmdType === "CmdDownloadVideoResult") {
console.log("收到视频下载响应:", message.content);
// 检查是否是我们正在等待的视频响应
const messageId = Object.keys(pendingVideoRequests).find(
id => pendingVideoRequests[id] === message.content.friendMessageId,
);
if (messageId) {
console.log("找到对应的消息ID:", messageId);
// 从待处理队列中移除
setPendingVideoRequests(prev => {
const newRequests = { ...prev };
delete newRequests[messageId];
return newRequests;
});
// 更新消息内容将视频URL添加到对应的消息中
setMessages(prevMessages => {
return prevMessages.map(msg => {
if (msg.id === Number(messageId)) {
try {
const msgContent =
typeof msg.content === "string"
? JSON.parse(msg.content)
: msg.content;
// 更新消息内容添加视频URL并移除加载状态
return {
...msg,
content: JSON.stringify({
...msgContent,
videoUrl: message.content.url,
isLoading: false,
}),
};
} catch (e) {
console.error("解析消息内容失败:", e);
}
}
return msg;
});
});
}
}
});
});
// 组件卸载时取消订阅
return () => {
unsubscribe();
};
}, [pendingVideoRequests]); // 依赖于pendingVideoRequests当队列变化时重新设置订阅
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
const handleSend = async () => {
if (!inputValue.trim()) return;
try {
const newMessage: ChatRecord = {
id: contract.id,
wechatAccountId: contract.wechatAccountId,
wechatFriendId: contract.id,
tenantId: 0,
accountId: 0,
synergyAccountId: 0,
content: inputValue,
msgType: 0,
msgSubType: 0,
msgSvrId: "",
isSend: false,
createTime: "",
isDeleted: false,
deleteTime: "",
sendStatus: 0,
wechatTime: 0,
origin: 0,
msgId: 0,
recalled: false,
};
setMessages(prev => [...prev, newMessage]);
onSendMessage(inputValue);
setInputValue("");
} catch (error) {
messageApi.error("发送失败");
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// 素材菜单项
const materialMenuItems = [
{
key: "text",
label: "文字素材",
icon: <span>📝</span>,
},
{
key: "audio",
label: "语音素材",
icon: <span>🎵</span>,
},
{
key: "image",
label: "图片素材",
icon: <span>🖼</span>,
},
{
key: "video",
label: "视频素材",
icon: <span>🎬</span>,
},
{
key: "link",
label: "链接素材",
icon: <span>🔗</span>,
},
{
key: "card",
label: "名片素材",
icon: <span>📇</span>,
},
];
const handleMaterialSelect = (key: string) => {
console.log("选择素材类型:", key);
setShowMaterialModal(true);
// 这里可以根据不同的素材类型显示不同的模态框
};
// 处理视频播放请求发送socket请求获取真实视频地址
const handleVideoPlayRequest = (tencentUrl: string, messageId: number) => {
// 生成请求ID (使用当前时间戳作为唯一标识)
const requestSeq = `${+new Date()}`;
console.log("发送视频下载请求:", { messageId, requestSeq });
// 构建socket请求数据
useWebSocketStore.getState().sendCommand("CmdDownloadVideo", {
chatroomMessageId: contract.chatroomId ? messageId : 0,
friendMessageId: contract.chatroomId ? 0 : messageId,
seq: requestSeq, // 使用唯一的请求ID
tencentUrl: tencentUrl,
wechatAccountId: contract.wechatAccountId,
});
// 将消息ID和请求序列号添加到待处理队列
setPendingVideoRequests(prev => ({
...prev,
[messageId]: messageId,
}));
// 更新消息状态为加载中
setMessages(prevMessages => {
return prevMessages.map(msg => {
if (msg.id === messageId) {
// 保存原始内容添加loading状态
const originalContent = msg.content;
return {
...msg,
content: JSON.stringify({
...JSON.parse(originalContent),
isLoading: true,
}),
};
}
return msg;
});
});
};
// 解析消息内容,判断消息类型并返回对应的渲染内容
const parseMessageContent = (content: string, msg: ChatRecord) => {
// 检查是否为表情包
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);
// 处理用户提供的JSON格式 {"previewImage":"https://...", "tencentUrl":"..."}
if (videoData.previewImage && videoData.tencentUrl) {
// 提取预览图URL去掉可能的引号
const previewImageUrl = videoData.previewImage.replace(/[`"']/g, "");
// 创建点击处理函数调用handleVideoPlayRequest发送socket请求获取真实视频地址
const handlePlayClick = (e: React.MouseEvent) => {
e.stopPropagation();
// 调用处理函数传入tencentUrl和消息ID
handleVideoPlayRequest(videoData.tencentUrl, msg.id);
};
// 检查是否已下载视频URL
if (videoData.videoUrl) {
// 已获取到视频URL显示视频播放器
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>
);
}
// 检查是否处于加载状态
if (videoData.isLoading) {
return (
<div className={styles.videoMessage}>
<div className={styles.videoContainer}>
<img
src={previewImageUrl}
alt="视频预览"
className={styles.videoThumbnail}
style={{
maxWidth: "100%",
borderRadius: "8px",
opacity: "0.7",
}}
/>
<div className={styles.videoPlayIcon}>
<div className={styles.loadingSpinner}></div>
</div>
</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" }}
/>
<div className={styles.videoPlayIcon}>
<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[]) => {
const groups: { time: string; messages: ChatRecord[] }[] = [];
messages.forEach(msg => {
// 使用 formatWechatTime 函数格式化时间戳
const formattedTime = formatWechatTime(msg.wechatTime);
groups.push({ time: formattedTime, messages: [msg] });
});
return groups;
};
const renderMessage = (msg: ChatRecord) => {
const isOwn = msg.isSend;
return (
<div
key={msg.id}
className={`${styles.messageItem} ${
isOwn ? styles.ownMessage : styles.otherMessage
}`}
>
<div className={styles.messageContent}>
{!isOwn && (
<Avatar
size={32}
src={contract.avatar}
icon={<UserOutlined />}
className={styles.messageAvatar}
/>
)}
<div className={styles.messageBubble}>
{!isOwn && (
<div className={styles.messageSender}>{msg.senderName}</div>
)}
{parseMessageContent(msg.content, msg)}
</div>
</div>
</div>
);
};
const chatMenuItems = [
{
key: "profile",
icon: <UserOutlined />,
label: "查看资料",
},
{
key: "call",
icon: <PhoneOutlined />,
label: "语音通话",
},
{
key: "video",
icon: <VideoCameraOutlined />,
label: "视频通话",
},
{
type: "divider",
},
{
key: "pin",
label: "置顶聊天",
},
{
key: "mute",
label: "消息免打扰",
},
{
type: "divider",
},
{
key: "clear",
danger: true,
label: "清空聊天记录",
},
];
// 模拟联系人详细信息
const contractInfo = {
name: contract.name,
avatar: contract.avatar,
phone: "13800138001",
email: "zhangsan@example.com",
department: "技术部",
position: "前端工程师",
company: "某某科技有限公司",
location: "北京市朝阳区",
joinDate: "2023-01-15",
status: "在线",
tags: ["技术专家", "前端", "React"],
bio: "专注于前端开发热爱新技术擅长React、Vue等框架。",
};
return (
<Layout className={styles.chatWindow}>
{contextHolder}
{/* 聊天主体区域 */}
<Layout className={styles.chatMain}>
{/* 聊天头部 */}
<Header className={styles.chatHeader}>
<div className={styles.chatHeaderInfo}>
<Avatar
size={40}
src={contract.avatar}
icon={
contract.type === "group" ? <TeamOutlined /> : <UserOutlined />
}
/>
<div
className={styles.chatHeaderDetails}
style={{
display: "flex",
}}
>
<div className={styles.chatHeaderName}>
{contract.name}
{contract.online && (
<span className={styles.chatHeaderOnlineStatus}>线</span>
)}
</div>
</div>
</div>
<Space>
<Tooltip title="语音通话">
<Button
type="text"
icon={<PhoneOutlined />}
className={styles.headerButton}
/>
</Tooltip>
<Tooltip title="视频通话">
<Button
type="text"
icon={<VideoCameraOutlined />}
className={styles.headerButton}
/>
</Tooltip>
<Dropdown menu={{ items: chatMenuItems }} trigger={["click"]}>
<Button
type="text"
icon={<MoreOutlined />}
className={styles.headerButton}
/>
</Dropdown>
</Space>
</Header>
{/* 聊天内容 */}
<Content className={styles.chatContent}>
<div className={styles.messagesContainer}>
{loading ? (
<div className={styles.loadingContainer}>
<div>...</div>
</div>
) : (
<>
{groupMessagesByTime(messages).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>
{/* 聊天输入 */}
<Footer className={styles.chatFooter}>
<div className={styles.inputContainer}>
<div className={styles.inputToolbar}>
<div className={styles.leftTool}>
<Tooltip title="表情">
<Button
type="text"
icon={<SmileOutlined />}
className={styles.toolbarButton}
/>
</Tooltip>
<Tooltip title="上传附件">
<Button
type="text"
icon={<FolderOutlined />}
className={styles.toolbarButton}
/>
</Tooltip>
<Tooltip title="收藏">
<Button
type="text"
icon={<StarOutlined />}
className={styles.toolbarButton}
/>
</Tooltip>
<Tooltip title="位置">
<Button
type="text"
icon={<LocationOutlined />}
className={styles.toolbarButton}
/>
</Tooltip>
<Tooltip title="语音">
<Button
type="text"
icon={<AudioOutlined />}
className={styles.toolbarButton}
/>
</Tooltip>
<Tooltip title="按住说话">
<Button
type="text"
icon={<AudioHoldOutlined />}
className={styles.toolbarButton}
style={{ position: "relative" }}
>
<span
style={{
position: "absolute",
top: "2px",
right: "2px",
fontSize: "8px",
color: "#52c41a",
fontWeight: "bold",
}}
>
H
</span>
</Button>
</Tooltip>
<Dropdown
overlay={
<Menu
items={materialMenuItems}
onClick={({ key }) => handleMaterialSelect(key)}
style={{
borderRadius: "8px",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
}}
/>
}
trigger={["click"]}
placement="topLeft"
>
<Button
type="text"
icon={<CodeSandboxOutlined />}
className={styles.toolbarButton}
/>
</Dropdown>
</div>
<div className={styles.rightTool}>
<div className={styles.rightToolItem}>
<ShareAltOutlined />
</div>
<div className={styles.rightToolItem}>
<MessageOutlined />
</div>
</div>
</div>
<div className={styles.inputArea}>
<TextArea
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入消息..."
autoSize={{ minRows: 1, maxRows: 4 }}
className={styles.messageInput}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
disabled={!inputValue.trim()}
className={styles.sendButton}
>
</Button>
</div>
<div className={styles.inputHint}>Ctrl+Enter换行</div>
</div>
</Footer>
</Layout>
{/* 右侧个人资料卡片 */}
{showProfile && (
<Sider width={280} className={styles.profileSider}>
<div className={styles.profileSiderContent}>
<div className={styles.profileHeader}>
<h3></h3>
<Button
type="text"
icon={<CloseOutlined />}
onClick={onToggleProfile}
className={styles.closeButton}
/>
</div>
<div className={styles.profileContent}>
{/* 基本信息 */}
<Card className={styles.profileCard}>
<div className={styles.profileBasic}>
<Avatar
size={80}
src={contractInfo.avatar}
icon={<UserOutlined />}
/>
<div className={styles.profileInfo}>
<h4>{contractInfo.name}</h4>
<p className={styles.profileStatus}>
<Badge status="success" text={contractInfo.status} />
</p>
<p className={styles.profilePosition}>
{contractInfo.position} · {contractInfo.department}
</p>
</div>
</div>
</Card>
{/* 联系信息 */}
<Card title="联系信息" className={styles.profileCard}>
<div className={styles.contractInfo}>
<div className={styles.contractItem}>
<PhoneOutlined />
<span className={styles.contractItemText}>
{contractInfo.phone}
</span>
</div>
<div className={styles.contractItem}>
<MailOutlined />
<span className={styles.contractItemText}>
{contractInfo.email}
</span>
</div>
<div className={styles.contractItem}>
<EnvironmentOutlined />
<span className={styles.contractItemText}>
{contractInfo.location}
</span>
</div>
<div className={styles.contractItem}>
<BankOutlined />
<span className={styles.contractItemText}>
{contractInfo.company}
</span>
</div>
<div className={styles.contractItem}>
<CalendarOutlined />
<span className={styles.contractItemText}>
{contractInfo.joinDate}
</span>
</div>
</div>
</Card>
{/* 标签 */}
<Card title="标签" className={styles.profileCard}>
<div className={styles.tagsContainer}>
{contractInfo.tags.map((tag, index) => (
<Tag key={index} color="blue">
{tag}
</Tag>
))}
</div>
</Card>
{/* 个人简介 */}
<Card title="个人简介" className={styles.profileCard}>
<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={showMaterialModal}
onCancel={() => setShowMaterialModal(false)}
footer={[
<Button key="cancel" onClick={() => setShowMaterialModal(false)}>
</Button>,
<Button
key="confirm"
type="primary"
onClick={() => setShowMaterialModal(false)}
>
</Button>,
]}
width={800}
>
<div style={{ display: "flex", height: "400px" }}>
{/* 左侧素材分类 */}
<div
style={{
width: "200px",
background: "#f5f5f5",
borderRight: "1px solid #e8e8e8",
}}
>
<div style={{ padding: "16px", borderBottom: "1px solid #e8e8e8" }}>
<h4 style={{ margin: 0, color: "#262626" }}></h4>
</div>
<div style={{ padding: "8px 0" }}>
<div
style={{
padding: "8px 16px",
cursor: "pointer",
background: "#e6f7ff",
borderLeft: "3px solid #1890ff",
color: "#1890ff",
}}
>
4
</div>
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
...
</div>
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
D2辅助
</div>
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
ROS反馈演示...
</div>
<div style={{ padding: "8px 16px", cursor: "pointer" }}>
...
</div>
</div>
<div style={{ padding: "16px", borderTop: "1px solid #e8e8e8" }}>
<h4 style={{ margin: 0, color: "#262626" }}></h4>
</div>
</div>
{/* 右侧内容区域 */}
<div style={{ flex: 1, padding: "16px" }}>
<div style={{ marginBottom: "16px" }}>
<Input.Search placeholder="昵称" style={{ width: "100%" }} />
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "300px",
color: "#8c8c8c",
}}
>
</div>
</div>
</div>
</Modal>
</Layout>
);
};
export default ChatWindow;