import React, { useState, useEffect, useMemo } from "react"; import { supabase } from "@/config/supabase"; import { Upload, Button, message, List, Switch, Space, Input, Tag, Modal, Image, Popconfirm, } from "antd"; import { UploadOutlined, FileTextOutlined, FileImageOutlined, FileMarkdownOutlined, FilePdfOutlined, FileWordOutlined, FileExcelOutlined, InboxOutlined, SearchOutlined, EditOutlined, FolderOutlined, } from "@ant-design/icons"; import MonacoEditor from "@monaco-editor/react"; import InfiniteScroll from 'react-infinite-scroll-component'; const { Dragger } = Upload; const { Search } = Input; // 文件类型配置 const FILE_TYPES = { 图片: ["image/jpeg", "image/png", "image/gif", "image/svg+xml"], 文档: [ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "text/plain", "text/markdown", ], 表格: [ "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", ], 代码: [ "text/html", "text/javascript", "text/css", "application/json", "text/jsx", "text/tsx", ], }; const StorageManager = () => { const [allFiles, setAllFiles] = useState([]); const [loading, setLoading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [fileContent, setFileContent] = useState(""); const [isPreview, setIsPreview] = useState(false); const [searchText, setSearchText] = useState(""); const [selectedType, setSelectedType] = useState("全部"); const [hasMore, setHasMore] = useState(true); const [displayFiles, setDisplayFiles] = useState([]); const INITIAL_LOAD_SIZE = 200; // 初始加载200条 const LOAD_MORE_SIZE = 100; // 每次加载100条 const [isRenaming, setIsRenaming] = useState(false); const [newFileName, setNewFileName] = useState(""); const [currentPath, setCurrentPath] = useState(""); // 添加当前路径状态 const [pathHistory, setPathHistory] = useState([]); // 添加路径历史记录 const [isUploading, setIsUploading] = useState(false); // 添加上传loading状态 // 文件图标映射 const getFileIcon = (file) => { const mimetype = file.metadata?.mimetype; const iconMap = { "text/plain": , "text/markdown": , "application/pdf": , "application/msword": , "application/vnd.openxmlformats-officedocument.wordprocessingml.document": , "application/vnd.ms-excel": , "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ( ), }; if (mimetype?.startsWith("image/")) { return ; } return iconMap[mimetype] || ; }; // 获取所有文件 const fetchAllFiles = async (isInitial = true) => { setLoading(true); try { const { data, error } = await supabase.storage .from("file") .list(currentPath, { // 使用当前路径 limit: isInitial ? INITIAL_LOAD_SIZE : LOAD_MORE_SIZE, offset: isInitial ? 0 : displayFiles.length, sortBy: { column: "created_at", order: "desc" }, }); if (error) throw error; // 对数据进行排序,文件夹在前 const sortedData = (data || []).sort((a, b) => { // 首先按照类型排序(文件夹在前) if (a.metadata?.isFolder !== b.metadata?.isFolder) { return a.metadata?.isFolder ? -1 : 1; } // 然后按照时间排序 return new Date(b.created_at) - new Date(a.created_at); }); if (isInitial) { setDisplayFiles(sortedData); } else { setDisplayFiles(prev => [...prev, ...sortedData]); } setHasMore(data?.length === (isInitial ? INITIAL_LOAD_SIZE : LOAD_MORE_SIZE)); } catch (error) { message.error(`获取文件列表失败: ${error.message}`); } finally { setLoading(false); } }; // 获取文件URL const getFileUrl = (fileName) => { const { data } = supabase.storage.from("file").getPublicUrl(fileName); return data.publicUrl; }; // 预览文件 const previewFile = async (file) => { try { if (file.metadata?.mimetype === 'application/pdf' || file.metadata?.mimetype.includes('msword') || file.metadata?.mimetype.includes('spreadsheet')) { window.open(getFileUrl(file.name), '_blank'); return; } // Existing preview logic for text files const { data, error } = await supabase.storage .from("file") .download(file.name); if (error) throw error; const content = await data.text(); setSelectedFile(file); setFileContent(content); } catch (error) { message.error("文件预览失败"); } }; // 上传文件配置 const uploadProps = { name: "file", multiple: true, showUploadList: false, customRequest: async ({ file, onSuccess, onError }) => { setIsUploading(true); // 开始上传时设置状态 try { const originalName = file.name; const fileName = handleFileName(originalName); // 检查文件是否存在 const fileExists = allFiles.some((f) => f.name === fileName); if (fileExists) { throw new Error("文件已存在"); } const { data, error } = await supabase.storage .from("file") .upload(fileName, file, { cacheControl: '3600', upsert: false }); if (error) throw error; message.success(`${originalName} 上传成功`); onSuccess(data); fetchAllFiles(); } catch (error) { message.error(`${file.name} 上传失败: ${error.message}`); onError(error); } finally { setIsUploading(false); // 上传完成后重置状态 } }, beforeUpload: (file) => { const isLt50M = file.size / 1024 / 1024 < 50; if (!isLt50M) { message.error("文件必须小于 50MB!"); return false; } return true; }, }; // 文件过滤逻辑 const filteredFiles = useMemo(() => { return displayFiles.filter((file) => { const matchesSearch = file.name .toLowerCase() .includes(searchText.toLowerCase()); if (selectedType === "全部") return matchesSearch; const mimetype = file.metadata?.mimetype || ''; const matchesType = FILE_TYPES[selectedType]?.some(type => mimetype.startsWith(type) || mimetype === type ); return matchesSearch && matchesType; }); }, [displayFiles, searchText, selectedType]); // 当前页的文件 // 获取文件类型统计 const typeStats = useMemo(() => { const stats = { 全部: 0, 其他: 0 }; // 初始化所有类型的计数为0 Object.keys(FILE_TYPES).forEach(type => { stats[type] = 0; }); // 统计每个文件的类型 displayFiles.forEach((file) => { const mimetype = file.metadata?.mimetype || ''; let matched = false; // 遍历所有文件类型配置 for (const [type, mimetypes] of Object.entries(FILE_TYPES)) { if (mimetypes.some(t => mimetype.startsWith(t) || mimetype === t)) { stats[type]++; matched = true; break; // 找到匹配后就跳出循环 } } // 如果没有匹配任何预定义类型,归类为"其他" if (!matched) { stats['其他']++; } // 更新总数 stats['全部']++; }); return stats; }, [displayFiles]); useEffect(() => { fetchAllFiles(); }, []); // 判断是否是��片 const isImage = (file) => { return file.metadata?.mimetype?.startsWith("image/"); }; // 判断是否是HTML const isHtml = (file) => { return file.metadata?.mimetype === "text/html"||file.metadata?.mimetype === "text/plain"; }; // 保存文件内容 const handleSaveContent = async () => { if (!selectedFile) return; try { // 创建 Blob 对象 const blob = new Blob([fileContent], { type: "text/plain" }); const file = new File([blob], selectedFile.name, { type: "text/plain" }); // 上传更新后的文件 const { error } = await supabase.storage .from("file") .update(selectedFile.name, file); if (error) throw error; message.success("保存成功"); fetchAllFiles(); // 刷新文件列表 } catch (error) { console.error("保存文件错误:", error); message.error(`保存失败: ${error.message}`); } }; // 更新编辑器内容 const handleEditorChange = (value) => { setFileContent(value); }; // 处理重命名 const handleRename = async () => { if (!selectedFile || !newFileName) return; try { // 检查新文件名是否已存在 const fileExists = allFiles.some((f) => f.name === newFileName); if (fileExists) { throw new Error("文件名已存在"); } // 获取原文件内容 const { data: fileData, error: downloadError } = await supabase.storage .from("file") .download(selectedFile.name); if (downloadError) throw downloadError; // 创建新文件 const { error: uploadError } = await supabase.storage .from("file") .upload(newFileName, fileData); if (uploadError) throw uploadError; // 删除旧文件 const { error: deleteError } = await supabase.storage .from("file") .remove([selectedFile.name]); if (deleteError) throw deleteError; message.success("重命名成功"); setIsRenaming(false); setSelectedFile({ ...selectedFile, name: newFileName }); fetchAllFiles(); } catch (error) { console.error("重命名错误:", error); message.error(`重命名失败: ${error.message}`); } }; // 开始重命名 const startRename = () => { setNewFileName(selectedFile.name); setIsRenaming(true); }; const handleDelete = async (fileName) => { try { const { error } = await supabase.storage.from("file").remove([fileName]); if (error) throw error; message.success("文件删除成功"); setDisplayFiles(prev => prev.filter(file => file.name !== fileName)); if (selectedFile?.name === fileName) { setSelectedFile(null); setFileContent(""); } } catch (error) { message.error(`删除失败: ${error.message}`); } }; const hasFilters = useMemo(() => { return searchText !== '' || selectedType !== '全部'; }, [searchText, selectedType]); const loadMoreFiles = () => { if (!hasMore || loading || hasFilters) return; // 有过滤条件不加载更多 fetchAllFiles(false); }; // 初始化加载 useEffect(() => { fetchAllFiles(true); }, []); const handleTypeChange = (type) => { setSelectedType(type); }; const handleFileName = (fileName) => { return fileName.replace(/\s+/g, '_'); }; // 渲染加载状态 const LoadingSpinner = () => (
); // 处理文件夹点击 const handleFolderClick = (folderName) => { const newPath = currentPath ? `${currentPath}/${folderName}` : folderName; setPathHistory(prev => [...prev, currentPath]); setCurrentPath(newPath); setDisplayFiles([]); // 清空当前列表 setHasMore(true); // 重置加载更多状态 fetchAllFiles(true); // 重新获取文件列表 }; // 返回上一级 const handleBack = () => { const previousPath = pathHistory[pathHistory.length - 1]; setPathHistory(prev => prev.slice(0, -1)); setCurrentPath(previousPath); setDisplayFiles([]); // 清空当前列表 setHasMore(true); // 重置加载更多状态 fetchAllFiles(true); // 重新获取文件列表 }; // 修改文件列表渲染 const renderFileList = () => (
{/* 面包屑导航样式优化 */} {currentPath && (
/ {currentPath}
)} } scrollableTarget="scrollableDiv" endMessage={

{displayFiles.length > 0 ? hasFilters ? "已显示所有匹配文件" : "已加载全部文件" : "暂无文件"}

} > ( file.metadata?.isFolder ? handleFolderClick(file.name) : previewFile(file)} >
) : isImage(file) ? ( {file.name} } /> ) : (
{getFileIcon(file)}
) } title={
{file.name} {!file.metadata?.isFolder && ( { e?.stopPropagation(); handleDelete(currentPath ? `${currentPath}/${file.name}` : file.name); }} okText="确认" cancelText="取消" > )}
} description={
{file.metadata?.isFolder ? '文件夹' : `类型: ${file.metadata?.mimetype}`} {!file.metadata?.isFolder && ( <> 大小: {(file.metadata?.size / 1024).toFixed(2)} KB 创建时间: {new Date(file.created_at).toLocaleString()} )}
} /> )} /> ); const renderTypeTags = () => (
{Object.entries({ 全部: null, ...FILE_TYPES, 其他: null }).map(([type]) => ( handleTypeChange(checked ? type : "全部")} className={`cursor-pointer ${typeStats[type] === 0 ? 'opacity-50' : ''}`} > {`${type} (${typeStats[type] || 0})`} ))}
); return (
{isUploading ? ( ) : ( )}

{isUploading ? '正在上传...' : '点击或拖拽文件上传'}

支持单个或批量上传,文件大小不超过50MB

setSearchText(e.target.value)} size="large" />
{renderTypeTags()}
{/* 文件列表 */} {loading && displayFiles.length === 0 ? (
) : ( renderFileList() )}
{/* 右侧预览区域 */}
{selectedFile ? ( <>
{selectedFile.name} )}
{isImage(selectedFile) ? (
{selectedFile.name} null, maskClassName: "backdrop-blur-sm", }} placeholder={
} />
) : isHtml(selectedFile) ? ( isPreview ? (