This commit is contained in:
liamzi
2024-12-27 11:47:51 +08:00
parent b9ea7218e3
commit 8d2383a8a9
12 changed files with 206 additions and 411 deletions

View File

@@ -24,6 +24,7 @@ import {
InboxOutlined,
SearchOutlined,
EditOutlined,
FolderOutlined,
} from "@ant-design/icons";
import MonacoEditor from "@monaco-editor/react";
import InfiniteScroll from 'react-infinite-scroll-component';
@@ -70,6 +71,8 @@ const StorageManager = () => {
const LOAD_MORE_SIZE = 100; // 每次加载100条
const [isRenaming, setIsRenaming] = useState(false);
const [newFileName, setNewFileName] = useState("");
const [currentPath, setCurrentPath] = useState(""); // 添加当前路径状态
const [pathHistory, setPathHistory] = useState([]); // 添加路径历史记录
// 文件图标映射
const getFileIcon = (file) => {
@@ -100,19 +103,29 @@ const StorageManager = () => {
try {
const { data, error } = await supabase.storage
.from("file")
.list("", {
.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;
if (isInitial) {
setDisplayFiles(data || []);
} else {
setDisplayFiles(prev => [...prev, ...(data || [])]);
}
// 对数据进行排序,文件夹在前
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) {
@@ -208,9 +221,11 @@ const StorageManager = () => {
if (selectedType === "全部") return matchesSearch;
const mimetype = file.metadata?.mimetype || '';
return matchesType && FILE_TYPES[selectedType]?.some(type =>
const matchesType = FILE_TYPES[selectedType]?.some(type =>
mimetype.startsWith(type) || mimetype === type
);
return matchesSearch && matchesType;
});
}, [displayFiles, searchText, selectedType]);
@@ -258,7 +273,7 @@ const StorageManager = () => {
fetchAllFiles();
}, []);
// 判断是否是
// 判断是否是<EFBFBD><EFBFBD>
const isImage = (file) => {
return file.metadata?.mimetype?.startsWith("image/");
};
@@ -349,12 +364,21 @@ const StorageManager = () => {
try {
const { error } = await supabase.storage.from("file").remove([fileName]);
if (error) throw error;
message.success("文件删除成功");
fetchAllFiles();
// 直接从本地状态中移除被删除的文件
setDisplayFiles(prev => prev.filter(file => file.name !== fileName));
// 如果删除的是当前选中的文件,清空预览
if (selectedFile?.name === fileName) {
setSelectedFile(null);
setFileContent("");
}
// 更新文件类型统计
// 注意typeStats 是通过 useMemo 自动计算的,不需要手动更新
} catch (error) {
message.error(`删除失败: ${error.message}`);
}
@@ -367,7 +391,7 @@ const StorageManager = () => {
// 加载更多数据
const loadMoreFiles = () => {
if (!hasMore || loading || hasFilters) return; // 有过滤条件<EFBFBD><EFBFBD>不加载更多
if (!hasMore || loading || hasFilters) return; // 有过滤条件不加载更多
fetchAllFiles(false);
};
@@ -406,21 +430,64 @@ const StorageManager = () => {
return '其他';
};
// 渲染文件列表
// 渲染加载状态
const LoadingSpinner = () => (
<div className="flex justify-center items-center p-4">
<div className="w-6 h-6 border-2 border-gray-200 border-t-blue-500 rounded-full animate-spin"></div>
</div>
);
// 处理文件夹点击
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 = () => (
<div className="flex-1 overflow-y-auto" id="scrollableDiv">
<div
className="flex-1 overflow-y-auto bg-white rounded-lg shadow-sm"
id="scrollableDiv"
>
{/* 面包屑导航 */}
{currentPath && (
<div className="p-4 border-b border-gray-100">
<div className="flex items-center space-x-2 text-sm">
<Button
type="link"
onClick={handleBack}
className="px-0"
>
返回上级
</Button>
<span className="text-gray-500">/</span>
<span className="text-gray-900">{currentPath}</span>
</div>
</div>
)}
<InfiniteScroll
dataLength={displayFiles.length}
next={loadMoreFiles}
hasMore={!hasFilters && hasMore} // 只在没有过滤条件时显示加载更多
loader={
<div className="text-center py-4">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent" />
</div>
}
hasMore={!hasFilters && hasMore}
loader={<LoadingSpinner />}
scrollableTarget="scrollableDiv"
endMessage={
<p className="text-center text-gray-500 py-4">
<p className="text-center text-gray-500 py-4 text-sm">
{displayFiles.length > 0
? hasFilters
? "已显示所有匹配文件"
@@ -430,66 +497,80 @@ const StorageManager = () => {
}
>
<List
loading={loading && displayFiles.length === 0}
loading={false}
dataSource={filteredFiles}
locale={{ emptyText: "暂无文件" }}
renderItem={(file) => (
<List.Item
className={`cursor-pointer hover:bg-gray-100 ${
selectedFile?.name === file.name ? "bg-blue-50" : ""
}`}
onClick={() => previewFile(file)}
actions={[
<Popconfirm
key="delete"
title="确认删除"
description={`是否确认删除文件 "${file.name}"?`}
onConfirm={(e) => {
e?.stopPropagation();
handleDelete(file.name);
}}
okText="确认"
cancelText="取消"
>
<Button
type="text"
danger
onClick={(e) => e.stopPropagation()}
>
删除
</Button>
</Popconfirm>,
]}
className={`
relative group cursor-pointer transition-colors duration-200
hover:bg-gray-50
${selectedFile?.name === file.name ? "bg-blue-50" : ""}
`}
onClick={() => file.metadata?.isFolder ? handleFolderClick(file.name) : previewFile(file)}
>
<List.Item.Meta
avatar={
isImage(file) ? (
file.metadata?.isFolder ? (
<div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg text-gray-500">
<FolderOutlined className="text-xl" />
</div>
) : isImage(file) ? (
<Image
src={getFileUrl(file.name)}
alt={file.name}
width={32}
height={32}
className="object-cover rounded"
width={40}
height={40}
className="object-cover rounded-lg"
preview={false}
loading="lazy"
placeholder={
<div className="w-8 h-8 bg-gray-200 rounded animate-pulse" />
<div className="w-10 h-10 bg-gray-100 rounded-lg animate-pulse" />
}
/>
) : (
getFileIcon(file)
<div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg text-gray-500">
{getFileIcon(file)}
</div>
)
}
title={file.name}
title={
<div className="flex items-center w-full pr-16">
<span className="text-sm font-medium text-gray-900 block w-full truncate group-hover:text-blue-500 transition-colors duration-200">
{file.name}
</span>
{!file.metadata?.isFolder && (
<Popconfirm
title="确认删除"
description={`是否确认删除${file.metadata?.isFolder ? '文件夹' : '文件'} "${file.name}"?`}
onConfirm={(e) => {
e?.stopPropagation();
handleDelete(currentPath ? `${currentPath}/${file.name}` : file.name);
}}
okText="确认"
cancelText="取消"
>
<Button
type="text"
danger
onClick={(e) => e.stopPropagation()}
className="absolute right-4 invisible group-hover:visible transition-all duration-200"
>
删除
</Button>
</Popconfirm>
)}
</div>
}
description={
<div className="text-xs text-gray-500">
<span>类型: {file.metadata?.mimetype}</span>
<span className="ml-2">
大小: {(file.metadata?.size / 1024).toFixed(2)} KB
</span>
<span className="ml-2">
创建时间: {new Date(file.created_at).toLocaleString()}
</span>
<div className="text-xs text-gray-500 space-x-4">
<span>{file.metadata?.isFolder ? '文件夹' : `类型: ${file.metadata?.mimetype}`}</span>
{!file.metadata?.isFolder && (
<>
<span>大小: {(file.metadata?.size / 1024).toFixed(2)} KB</span>
<span>创建时间: {new Date(file.created_at).toLocaleString()}</span>
</>
)}
</div>
}
/>
@@ -519,51 +600,68 @@ const StorageManager = () => {
return (
<div className="flex h-screen bg-gray-50">
{/* 左侧文件列表 */}
<div className="w-1/3 p-2 border-r border-gray-200 flex flex-col">
<div className="mb-4 h-[150px]">
<div className="w-1/3 p-4 flex flex-col space-y-4">
{/* 上传区域 */}
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
<Dragger
{...uploadProps}
className="bg-white p-2 rounded-lg shadow-sm"
className="px-6 py-8 hover:bg-gray-50 transition-colors group"
>
<p className="text-base">点击或者拖拽文件到此区域上传</p>
<p className="ant-upload-hint text-xs text-gray-500">
支持单个或批量上传文件大小不超过50MB
</p>
<div className="space-y-3 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-50 group-hover:bg-blue-100 transition-colors">
<InboxOutlined className="text-3xl text-blue-500" />
</div>
<div>
<p className="text-base font-medium text-gray-900">
点击或拖拽文件上传
</p>
<p className="mt-1 text-sm text-gray-500">
支持单个或批量上传文件大小不超过50MB
</p>
</div>
</div>
</Dragger>
</div>
{/* 搜索和筛选区域 */}
<div className="mb-4 space-y-3">
<div className="space-y-3">
<Search
placeholder="搜索文件名..."
allowClear
onChange={(e) => {
setSearchText(e.target.value);
}}
onChange={(e) => setSearchText(e.target.value)}
className="w-full"
size="large"
/>
{renderTypeTags()}
<div className="bg-white p-3 rounded-lg shadow-sm">
{renderTypeTags()}
</div>
</div>
{/* 文件列表 */}
{renderFileList()}
{loading && displayFiles.length === 0 ? (
<div className="flex-1 flex items-center justify-center">
<LoadingSpinner />
</div>
) : (
renderFileList()
)}
</div>
{/* 右侧预览区域 */}
<div className="flex-1 p-4 overflow-y-auto bg-white">
<div className="flex-1 p-4 bg-white border-l border-gray-200">
{selectedFile ? (
<>
<div className="mb-4 border-b pb-4">
<div className="mb-4 pb-4 border-b border-gray-200">
<Space size="middle" align="center">
<Space>
<span className="font-medium text-lg">
<span className="text-lg font-medium text-gray-900">
{selectedFile.name}
</span>
<Button
type="text"
icon={<EditOutlined />}
onClick={startRename}
className="text-gray-500 hover:text-blue-500"
/>
</Space>
{isHtml(selectedFile) && (
@@ -575,7 +673,11 @@ const StorageManager = () => {
/>
)}
{!isImage(selectedFile) && (
<Button type="primary" onClick={handleSaveContent}>
<Button
type="primary"
onClick={handleSaveContent}
className="bg-blue-500 hover:bg-blue-600"
>
保存
</Button>
)}
@@ -588,15 +690,15 @@ const StorageManager = () => {
<Image
src={getFileUrl(selectedFile.name)}
alt={selectedFile.name}
className="max-w-full max-h-[80vh] object-contain"
className="max-w-full max-h-[80vh] object-contain rounded-lg"
loading="lazy"
preview={{
toolbarRender: () => null, // 隐藏底部工具栏
toolbarRender: () => null,
maskClassName: "backdrop-blur-sm",
}}
placeholder={
<div className="w-full h-[80vh] bg-gray-100 rounded flex items-center justify-center">
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
<div className="w-full h-[80vh] bg-gray-50 rounded-lg flex items-center justify-center">
<LoadingSpinner />
</div>
}
/>
@@ -605,7 +707,7 @@ const StorageManager = () => {
isPreview ? (
<iframe
srcDoc={fileContent}
className="w-full h-[calc(100vh-200px)] border rounded"
className="w-full h-[calc(100vh-200px)] border rounded-lg"
/>
) : (
<MonacoEditor
@@ -616,7 +718,10 @@ const StorageManager = () => {
onChange={handleEditorChange}
options={{
minimap: { enabled: true },
roundedSelection: true,
padding: { top: 16 },
}}
className="rounded-lg overflow-hidden"
/>
)
) : (
@@ -628,14 +733,20 @@ const StorageManager = () => {
onChange={handleEditorChange}
options={{
minimap: { enabled: false },
roundedSelection: true,
padding: { top: 16 },
}}
className="rounded-lg overflow-hidden"
/>
)}
</div>
</>
) : (
<div className="h-full flex items-center justify-center text-gray-400">
选择文件以预览
<div className="text-center">
<InboxOutlined className="text-4xl mb-2" />
<p>选择文件以预览</p>
</div>
</div>
)}
</div>
@@ -654,6 +765,7 @@ const StorageManager = () => {
onChange={(e) => setNewFileName(e.target.value)}
placeholder="请输入新文件名"
autoFocus
className="mt-4"
/>
</Modal>
</div>