feat(MessageRecord): 添加右键菜单功能支持消息操作
实现消息记录的右键菜单功能,包括复制、转发、删除和重发等操作。添加ClickMenu组件处理菜单显示和位置调整,并在MessageRecord中集成右键事件处理逻辑。支持暗色主题适配和边界检测防止菜单溢出屏幕。
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
.contextMenu {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e8e8e8;
|
||||
overflow: hidden;
|
||||
min-width: 120px;
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
|
||||
.menu {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
:global(.ant-menu-item) {
|
||||
padding: 8px 16px;
|
||||
margin: 0;
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
border-radius: 0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&:global(.ant-menu-item-danger) {
|
||||
color: #ff4d4f;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.ant-menu-item-disabled) {
|
||||
color: #bfbfbf;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-menu-item-icon) {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-menu-item-divider) {
|
||||
margin: 4px 0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题支持
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.contextMenu {
|
||||
background: #2f2f2f;
|
||||
border-color: #434343;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
|
||||
.menu {
|
||||
:global(.ant-menu-item) {
|
||||
color: #ffffff;
|
||||
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
}
|
||||
|
||||
&:global(.ant-menu-item-danger) {
|
||||
color: #ff7875;
|
||||
|
||||
&:hover {
|
||||
background-color: #2a1215;
|
||||
color: #ff7875;
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.ant-menu-item-disabled) {
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-menu-item-divider) {
|
||||
background-color: #434343;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Menu, message } from "antd";
|
||||
import {
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
ShareAltOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ChatRecord } from "@/pages/pc/ckbox/data";
|
||||
import styles from "./ClickMenu.module.scss";
|
||||
|
||||
interface ClickMenuProps {
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
messageData: ChatRecord | null;
|
||||
onClose: () => void;
|
||||
onCopy?: (content: string) => void;
|
||||
onDelete?: (messageId: string) => void;
|
||||
onForward?: (messageData: ChatRecord) => void;
|
||||
onRetry?: (messageData: ChatRecord) => void;
|
||||
}
|
||||
|
||||
const ClickMenu: React.FC<ClickMenuProps> = ({
|
||||
visible,
|
||||
x,
|
||||
y,
|
||||
messageData,
|
||||
onClose,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onForward,
|
||||
onRetry,
|
||||
}) => {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [position, setPosition] = useState({ x, y });
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && menuRef.current) {
|
||||
const menuRect = menuRef.current.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
let adjustedX = x;
|
||||
let adjustedY = y;
|
||||
|
||||
// 防止菜单超出屏幕右边界
|
||||
if (x + menuRect.width > windowWidth) {
|
||||
adjustedX = windowWidth - menuRect.width - 10;
|
||||
}
|
||||
|
||||
// 防止菜单超出屏幕下边界
|
||||
if (y + menuRect.height > windowHeight) {
|
||||
adjustedY = windowHeight - menuRect.height - 10;
|
||||
}
|
||||
|
||||
setPosition({ x: adjustedX, y: adjustedY });
|
||||
}
|
||||
}, [visible, x, y]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (visible) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleEscKey);
|
||||
};
|
||||
}, [visible, onClose]);
|
||||
|
||||
if (!visible || !messageData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
if (messageData.content && onCopy) {
|
||||
// 提取纯文本内容,去除HTML标签
|
||||
const textContent = messageData.content.replace(/<[^>]*>/g, "");
|
||||
onCopy(textContent);
|
||||
navigator.clipboard
|
||||
.writeText(textContent)
|
||||
.then(() => {
|
||||
message.success("已复制到剪贴板");
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("复制失败");
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (messageData.id && onDelete) {
|
||||
onDelete(messageData.id.toString());
|
||||
message.success("消息已删除");
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleForward = () => {
|
||||
if (onForward) {
|
||||
onForward(messageData);
|
||||
message.info("转发功能待实现");
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
if (onRetry) {
|
||||
onRetry(messageData);
|
||||
message.info("重新发送");
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: "copy",
|
||||
icon: <CopyOutlined />,
|
||||
label: "转发",
|
||||
onClick: handleCopy,
|
||||
disabled: !messageData.content || messageData.msgType === 3, // 图片消息不能复制文本
|
||||
},
|
||||
{
|
||||
key: "forward",
|
||||
icon: <ShareAltOutlined />,
|
||||
label: "复制",
|
||||
onClick: handleForward,
|
||||
},
|
||||
{
|
||||
key: "copy",
|
||||
icon: <CopyOutlined />,
|
||||
label: "多条转发",
|
||||
onClick: handleCopy,
|
||||
disabled: !messageData.content || messageData.msgType === 3, // 图片消息不能复制文本
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
icon: <DeleteOutlined />,
|
||||
label: "引用",
|
||||
onClick: handleDelete,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
icon: <DeleteOutlined />,
|
||||
label: "撤回",
|
||||
onClick: handleDelete,
|
||||
},
|
||||
];
|
||||
|
||||
// 如果是自己发送的消息且发送失败,显示重试选项
|
||||
if (messageData.isSend && messageData.status === "failed") {
|
||||
menuItems.unshift({
|
||||
key: "retry",
|
||||
icon: <ReloadOutlined />,
|
||||
label: "重新发送",
|
||||
onClick: handleRetry,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={styles.contextMenu}
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
items={menuItems}
|
||||
mode="vertical"
|
||||
selectable={false}
|
||||
className={styles.menu}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClickMenu;
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Avatar, Divider } from "antd";
|
||||
import { UserOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||
import AudioMessage from "./components/AudioMessage/AudioMessage";
|
||||
import SmallProgramMessage from "./components/SmallProgramMessage";
|
||||
import VideoMessage from "./components/VideoMessage";
|
||||
import ClickMenu from "./components/ClickMeau";
|
||||
import { ChatRecord, ContractData, weChatGroup } from "@/pages/pc/ckbox/data";
|
||||
import { formatWechatTime } from "@/utils/common";
|
||||
import { getEmojiPath } from "@/components/EmojiSeclection/wechatEmoji";
|
||||
@@ -16,6 +17,14 @@ interface MessageRecordProps {
|
||||
const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 右键菜单状态
|
||||
const [contextMenu, setContextMenu] = useState({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
messageData: null as ChatRecord | null,
|
||||
});
|
||||
|
||||
const currentMessages = useWeChatStore(state => state.currentMessages);
|
||||
const loadChatMessages = useWeChatStore(state => state.loadChatMessages);
|
||||
const messagesLoading = useWeChatStore(state => state.messagesLoading);
|
||||
@@ -454,6 +463,46 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 右键菜单事件处理
|
||||
const handleContextMenu = (e: React.MouseEvent, msg: ChatRecord) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({
|
||||
visible: true,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
messageData: msg,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseContextMenu = () => {
|
||||
setContextMenu({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
messageData: null,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyMessage = (content: string) => {
|
||||
// 复制消息内容的处理逻辑
|
||||
console.log("复制消息:", content);
|
||||
};
|
||||
|
||||
const handleDeleteMessage = (messageId: string) => {
|
||||
// 删除消息的处理逻辑
|
||||
console.log("删除消息:", messageId);
|
||||
};
|
||||
|
||||
const handleForwardMessage = (messageData: ChatRecord) => {
|
||||
// 转发消息的处理逻辑
|
||||
console.log("转发消息:", messageData);
|
||||
};
|
||||
|
||||
const handleRetryMessage = (messageData: ChatRecord) => {
|
||||
// 重试发送消息的处理逻辑
|
||||
console.log("重试发送:", messageData);
|
||||
};
|
||||
|
||||
// 用于分组消息并添加时间戳的辅助函数
|
||||
const groupMessagesByTime = (messages: ChatRecord[]) => {
|
||||
return messages
|
||||
@@ -477,6 +526,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
className={`${styles.messageItem} ${
|
||||
isOwn ? styles.ownMessage : styles.otherMessage
|
||||
}`}
|
||||
onContextMenu={e => handleContextMenu(e, msg)}
|
||||
>
|
||||
<div className={styles.messageContent}>
|
||||
{/* 如果不是群聊 */}
|
||||
@@ -587,6 +637,19 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
|
||||
{/* 右键菜单组件 */}
|
||||
<ClickMenu
|
||||
visible={contextMenu.visible}
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
messageData={contextMenu.messageData}
|
||||
onClose={handleCloseContextMenu}
|
||||
onCopy={handleCopyMessage}
|
||||
onDelete={handleDeleteMessage}
|
||||
onForward={handleForwardMessage}
|
||||
onRetry={handleRetryMessage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user