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 = () => (
) : isImage(file) ? (
}
/>
) : (
{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
{/* 文件列表 */}
{loading && displayFiles.length === 0 ? (
) : (
renderFileList()
)}
{/* 右侧预览区域 */}
{selectedFile ? (
<>
{selectedFile.name}
}
onClick={startRename}
className="text-gray-500 dark:text-gray-100 hover:text-blue-500"
/>
{isHtml(selectedFile) && (
)}
{!isImage(selectedFile) && (
)}
{isImage(selectedFile) ? (
null,
maskClassName: "backdrop-blur-sm",
}}
placeholder={
}
/>
) : isHtml(selectedFile) ? (
isPreview ? (
) : (
)
) : (
)}
>
) : (
)}
{/* 重命名对话框 */}
setIsRenaming(false)}
okText="确认"
cancelText="取消"
>
setNewFileName(e.target.value)}
placeholder="请输入新文件名"
autoFocus
className="mt-4"
/>
);
};
export default StorageManager;