Files
cunkebao_v3/Cunkebao/src/pages/pc/ckbox/components/ChatWindow/index.tsx

634 lines
19 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,
Divider,
Badge,
Card,
Tag,
Row,
Col,
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,
CodeSandboxOutlined,
MessageOutlined,
} from "@ant-design/icons";
import dayjs from "dayjs";
import { ChatSession, MessageData, MessageType } from "../../data";
// import { getChatHistory, sendMessage } from "../api";
import styles from "./ChatWindow.module.scss";
const { Header, Content, Footer, Sider } = Layout;
const { TextArea } = Input;
interface ChatWindowProps {
chat: ChatSession;
onSendMessage: (message: string) => void;
showProfile?: boolean;
onToggleProfile?: () => void;
}
const ChatWindow: React.FC<ChatWindowProps> = ({
chat,
onSendMessage,
showProfile = true,
onToggleProfile,
}) => {
const [messages, setMessages] = useState<MessageData[]>([]);
const [inputValue, setInputValue] = useState("");
const [loading, setLoading] = useState(false);
const [showMaterialModal, setShowMaterialModal] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetchChatHistory();
}, [chat.id]);
useEffect(() => {
scrollToBottom();
}, [messages]);
const fetchChatHistory = async () => {
try {
setLoading(true);
// 模拟聊天历史数据
const mockMessages: MessageData[] = [
{
id: "1",
senderId: "other",
senderName: chat.name,
content: "你好,请问有什么可以帮助您的吗?",
type: MessageType.TEXT,
timestamp: dayjs().subtract(10, "minute").toISOString(),
isRead: true,
},
{
id: "2",
senderId: "me",
senderName: "我",
content: "我想了解一下你们的产品",
type: MessageType.TEXT,
timestamp: dayjs().subtract(8, "minute").toISOString(),
isRead: true,
},
{
id: "3",
senderId: "other",
senderName: chat.name,
content: "好的,我来为您详细介绍",
type: MessageType.TEXT,
timestamp: dayjs().subtract(5, "minute").toISOString(),
isRead: true,
},
];
setMessages(mockMessages);
} catch (error) {
message.error("获取聊天记录失败");
} finally {
setLoading(false);
}
};
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
const handleSend = async () => {
if (!inputValue.trim()) return;
try {
const newMessage: MessageData = {
id: Date.now().toString(),
senderId: "me",
senderName: "我",
content: inputValue,
type: MessageType.TEXT,
timestamp: dayjs().toISOString(),
isRead: false,
};
setMessages(prev => [...prev, newMessage]);
onSendMessage(inputValue);
setInputValue("");
} catch (error) {
message.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);
// 这里可以根据不同的素材类型显示不同的模态框
};
const renderMessage = (msg: MessageData) => {
const isOwn = msg.senderId === "me";
return (
<div
key={msg.id}
className={`${styles.messageItem} ${
isOwn ? styles.ownMessage : styles.otherMessage
}`}
>
<div className={styles.messageContent}>
{!isOwn && (
<Avatar
size={32}
src={chat.avatar}
icon={<UserOutlined />}
className={styles.messageAvatar}
/>
)}
<div className={styles.messageBubble}>
{!isOwn && (
<div className={styles.messageSender}>{msg.senderName}</div>
)}
<div className={styles.messageText}>{msg.content}</div>
<div className={styles.messageTime}>
{dayjs(msg.timestamp).format("HH:mm")}
</div>
</div>
</div>
</div>
);
};
const chatMenu = (
<Menu>
<Menu.Item key="profile" icon={<UserOutlined />}>
</Menu.Item>
<Menu.Item key="call" icon={<PhoneOutlined />}>
</Menu.Item>
<Menu.Item key="video" icon={<VideoCameraOutlined />}>
</Menu.Item>
<Menu.Divider />
<Menu.Item key="pin"></Menu.Item>
<Menu.Item key="mute"></Menu.Item>
<Menu.Divider />
<Menu.Item key="clear" danger>
</Menu.Item>
</Menu>
);
// 模拟联系人详细信息
const contactInfo = {
name: chat.name,
avatar: chat.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}>
{/* 聊天主体区域 */}
<Layout className={styles.chatMain}>
{/* 聊天头部 */}
<Header className={styles.chatHeader}>
<div className={styles.chatHeaderInfo}>
<Avatar
size={40}
src={chat.avatar}
icon={chat.type === "group" ? <TeamOutlined /> : <UserOutlined />}
/>
<div
className={styles.chatHeaderDetails}
style={{
display: "flex",
}}
>
<div className={styles.chatHeaderName}>
{chat.name}
{chat.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 overlay={chatMenu} 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>
) : (
<>
{messages.map(renderMessage)}
<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={contactInfo.avatar}
icon={<UserOutlined />}
/>
<div className={styles.profileInfo}>
<h4>{contactInfo.name}</h4>
<p className={styles.profileStatus}>
<Badge status="success" text={contactInfo.status} />
</p>
<p className={styles.profilePosition}>
{contactInfo.position} · {contactInfo.department}
</p>
</div>
</div>
</Card>
{/* 联系信息 */}
<Card title="联系信息" className={styles.profileCard}>
<div className={styles.contactInfo}>
<div className={styles.contactItem}>
<PhoneOutlined />
<span className={styles.contactItemText}>
{contactInfo.phone}
</span>
</div>
<div className={styles.contactItem}>
<MailOutlined />
<span className={styles.contactItemText}>
{contactInfo.email}
</span>
</div>
<div className={styles.contactItem}>
<EnvironmentOutlined />
<span className={styles.contactItemText}>
{contactInfo.location}
</span>
</div>
<div className={styles.contactItem}>
<BankOutlined />
<span className={styles.contactItemText}>
{contactInfo.company}
</span>
</div>
<div className={styles.contactItem}>
<CalendarOutlined />
<span className={styles.contactItemText}>
{contactInfo.joinDate}
</span>
</div>
</div>
</Card>
{/* 标签 */}
<Card title="标签" className={styles.profileCard}>
<div className={styles.tagsContainer}>
{contactInfo.tags.map((tag, index) => (
<Tag key={index} color="blue">
{tag}
</Tag>
))}
</div>
</Card>
{/* 个人简介 */}
<Card title="个人简介" className={styles.profileCard}>
<p className={styles.bioText}>{contactInfo.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}
bodyStyle={{ padding: 0 }}
>
<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;