Files
GameServerManager/client/src/components/FileContextMenu.tsx
2025-07-16 16:19:05 +08:00

271 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 from 'react'
import {
EditOutlined,
DeleteOutlined,
CopyOutlined,
ScissorOutlined,
DownloadOutlined,
FileOutlined,
FolderOpenOutlined,
EyeOutlined,
SnippetsOutlined,
FileZipOutlined,
FolderOutlined,
ConsoleSqlOutlined,
SoundOutlined
} from '@ant-design/icons'
import { FileItem } from '@/types/file'
interface FileContextMenuProps {
children: React.ReactNode
file: FileItem
selectedFiles: Set<string>
clipboard: {
items: string[]
operation: 'copy' | 'cut' | null
}
// 菜单全局状态props
globalContextMenuInfo: {
file: FileItem | null
position: { x: number; y: number }
} | null
onOpen: (file: FileItem) => void
onRename: (file: FileItem) => void
onDelete: (files: FileItem[]) => void
onDownload: (file: FileItem) => void
onCopy: (files: FileItem[]) => void
onCut: (files: FileItem[]) => void
onPaste: () => void
onView: (file: FileItem) => void
onCompress: (files: FileItem[]) => void
onExtract: (file: FileItem) => void
onOpenTerminal: (file: FileItem) => void
onAddToPlaylist?: (files: FileItem[]) => void
// 菜单全局状态props
setGlobalContextMenuInfo: React.Dispatch<React.SetStateAction<{
file: FileItem | null
position: { x: number; y: number }
} | null>>
}
export const FileContextMenu: React.FC<FileContextMenuProps> = ({
children,
file,
selectedFiles,
clipboard,
globalContextMenuInfo,
onOpen,
onRename,
onDelete,
onDownload,
onCopy,
onCut,
onPaste,
onView,
onCompress,
onExtract,
onOpenTerminal,
onAddToPlaylist,
setGlobalContextMenuInfo
}) => {
const isSelected = selectedFiles.has(file.path)
const selectedCount = selectedFiles.size
const isMultipleSelected = selectedCount > 1
const contextMenuVisible = globalContextMenuInfo?.file?.path === file.path;
const contextMenuPosition = globalContextMenuInfo?.position || { x: 0, y: 0 };
const getSelectedFiles = (): FileItem[] => {
if (isSelected && isMultipleSelected) {
// 如果当前文件被选中且有多个选中项,操作所有选中的文件
return Array.from(selectedFiles).map(path => ({ ...file, path }))
} else {
// 否则只操作当前文件
return [file]
}
}
// 检查是否为音频文件
const isAudioFile = (fileName: string): boolean => {
const supportedFormats = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac']
return supportedFormats.some(format =>
fileName.toLowerCase().endsWith(format)
)
}
// 检查选中的文件中是否有音频文件
const hasAudioFiles = (): boolean => {
const files = getSelectedFiles()
return files.some(f => f.type === 'file' && isAudioFile(f.name))
}
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
setGlobalContextMenuInfo({file, position: { x: event.clientX, y: event.clientY }});
//日志
console.log('显示右键菜单:', file.name);
};
const handleMenuClick = () => {
setGlobalContextMenuInfo(null);
};
React.useEffect(() => {
if (contextMenuVisible) {
const handleClickOutside = () => setGlobalContextMenuInfo(null);
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
}, [contextMenuVisible, setGlobalContextMenuInfo]);
return (
<>
<div onContextMenu={handleContextMenu}>
{children}
</div>
{contextMenuVisible && (
<div
className="fixed bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-2 min-w-[160px]"
style={{
left: contextMenuPosition.x,
top: contextMenuPosition.y,
zIndex: 1000
}}
onClick={handleMenuClick}
>
{/* 打开/查看 */}
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => {
console.log('右键菜单 - 打开:', file.name)
onOpen(file)
}}
>
{file.type === 'directory' ? <FolderOpenOutlined className="mr-2" /> : <FileOutlined className="mr-2" />}
{file.type === 'directory' ? '打开文件夹' : '打开文件'}
</div>
{/* 查看(仅文件) */}
{file.type === 'file' && (
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onView(file)}
>
<EyeOutlined className="mr-2" />
</div>
)}
{/* 从此文件夹处打开终端(仅文件夹) */}
{file.type === 'directory' && (
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onOpenTerminal(file)}
>
<ConsoleSqlOutlined className="mr-2" />
</div>
)}
{/* 添加到播放列表(仅音频文件) */}
{onAddToPlaylist && hasAudioFiles() && (
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onAddToPlaylist(getSelectedFiles())}
>
<SoundOutlined className="mr-2" />
{isMultipleSelected ? `添加 ${selectedCount} 项到播放列表` : '添加到播放列表'}
</div>
)}
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
{/* 重命名(仅单个文件) */}
{!isMultipleSelected && (
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onRename(file)}
>
<EditOutlined className="mr-2" />
</div>
)}
{/* 复制 */}
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onCopy(getSelectedFiles())}
>
<CopyOutlined className="mr-2" />
{isMultipleSelected ? `复制 ${selectedCount}` : '复制'}
</div>
{/* 剪切 */}
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onCut(getSelectedFiles())}
>
<ScissorOutlined className="mr-2" />
{isMultipleSelected ? `剪切 ${selectedCount}` : '剪切'}
</div>
{/* 粘贴 */}
{clipboard.operation && clipboard.items.length > 0 && (
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={onPaste}
>
<SnippetsOutlined className="mr-2" />
{clipboard.items.length}
</div>
)}
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
{/* 压缩 */}
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onCompress(getSelectedFiles())}
>
<FileZipOutlined className="mr-2" />
{isMultipleSelected ? `压缩 ${selectedCount}` : '压缩'}
</div>
{/* 解压仅zip文件 */}
{file.type === 'file' && !isMultipleSelected &&
file.name.toLowerCase().endsWith('.zip') && (
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onExtract(file)}
>
<FolderOutlined className="mr-2" />
</div>
)}
<div className="border-t border-gray-200 dark:border-gray-600 my-1"></div>
{/* 下载(仅文件) */}
{file.type === 'file' && !isMultipleSelected && (
<div
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex items-center"
onClick={() => onDownload(file)}
>
<DownloadOutlined className="mr-2" />
</div>
)}
{/* 删除 */}
<div
className="px-4 py-2 hover:bg-red-100 dark:hover:bg-red-900/30 cursor-pointer flex items-center text-red-600 dark:text-red-400"
onClick={() => onDelete(getSelectedFiles())}
>
<DeleteOutlined className="mr-2" />
{isMultipleSelected ? `删除 ${selectedCount}` : '删除'}
</div>
</div>
)}
</>
)
}