780 lines
24 KiB
JavaScript
780 lines
24 KiB
JavaScript
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": <FileTextOutlined />,
|
||
"text/markdown": <FileMarkdownOutlined />,
|
||
"application/pdf": <FilePdfOutlined />,
|
||
"application/msword": <FileWordOutlined />,
|
||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||
<FileWordOutlined />,
|
||
"application/vnd.ms-excel": <FileExcelOutlined />,
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": (
|
||
<FileExcelOutlined />
|
||
),
|
||
};
|
||
|
||
if (mimetype?.startsWith("image/")) {
|
||
return <FileImageOutlined />;
|
||
}
|
||
|
||
return iconMap[mimetype] || <FileTextOutlined />;
|
||
};
|
||
|
||
// 获取所有文件
|
||
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();
|
||
}, []);
|
||
|
||
// 判断是否是<E590A6><E698AF>片
|
||
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("");
|
||
}
|
||
|
||
// 更新文件类型统计
|
||
// 注意:typeStats 是通过 useMemo 自动计算的,不需要手动更新
|
||
|
||
} 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 handleSearch = (value) => {
|
||
setSearchText(value);
|
||
// 不需要重新调用 fetchAllFiles,因为搜索是在前端过滤
|
||
};
|
||
|
||
const handleTypeChange = (type) => {
|
||
setSelectedType(type);
|
||
// 不需要重新调用 fetchAllFiles,因为类型筛选是在前端过滤
|
||
};
|
||
|
||
// 处理文件名中的空格
|
||
const handleFileName = (fileName) => {
|
||
// 替换空格为下划线或编码空格
|
||
return fileName.replace(/\s+/g, '_');
|
||
};
|
||
|
||
// 修改文件类型判断
|
||
const getFileType = (mimetype) => {
|
||
if (!mimetype) return '其他';
|
||
|
||
for (const [type, mimetypes] of Object.entries(FILE_TYPES)) {
|
||
if (mimetypes.some(t => mimetype.startsWith(t) || mimetype === t)) {
|
||
return type;
|
||
}
|
||
}
|
||
|
||
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 rounded-lg shadow-sm "
|
||
id="scrollableDiv"
|
||
>
|
||
{/* 面包屑导航样式优化 */}
|
||
{currentPath && (
|
||
<div className="p-4 border-b border-gray-100 dark:border-gray-700">
|
||
<div className="flex items-center space-x-2 text-sm">
|
||
<Button
|
||
type="link"
|
||
onClick={handleBack}
|
||
className="px-0 text-blue-500 dark:text-blue-400"
|
||
>
|
||
返回上级
|
||
</Button>
|
||
<span className="text-gray-500 dark:text-gray-400">/</span>
|
||
<span className="text-gray-900 dark:text-gray-100">{currentPath}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<InfiniteScroll
|
||
dataLength={displayFiles.length}
|
||
next={loadMoreFiles}
|
||
hasMore={!hasFilters && hasMore}
|
||
loader={<LoadingSpinner />}
|
||
scrollableTarget="scrollableDiv"
|
||
endMessage={
|
||
<p className="text-center text-gray-500 py-4 text-sm">
|
||
{displayFiles.length > 0
|
||
? hasFilters
|
||
? "已显示所有匹配文件"
|
||
: "已加载全部文件"
|
||
: "暂无文件"}
|
||
</p>
|
||
}
|
||
>
|
||
<List
|
||
loading={false}
|
||
dataSource={filteredFiles}
|
||
locale={{ emptyText: "暂无文件" }}
|
||
renderItem={(file) => (
|
||
<List.Item
|
||
className={`
|
||
relative group cursor-pointer transition-colors duration-200
|
||
hover:bg-gray-50 dark:hover:bg-gray-700
|
||
${selectedFile?.name === file.name ? "bg-blue-50 dark:bg-gray-700" : ""}
|
||
`}
|
||
onClick={() => file.metadata?.isFolder ? handleFolderClick(file.name) : previewFile(file)}
|
||
>
|
||
<List.Item.Meta
|
||
avatar={
|
||
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={40}
|
||
height={40}
|
||
className="object-cover rounded-lg"
|
||
preview={false}
|
||
loading="lazy"
|
||
placeholder={
|
||
<div className="w-10 h-10 bg-gray-100 rounded-lg animate-pulse" />
|
||
}
|
||
/>
|
||
) : (
|
||
<div className="w-10 h-10 flex items-center justify-center bg-gray-100 rounded-lg text-gray-500">
|
||
{getFileIcon(file)}
|
||
</div>
|
||
)
|
||
}
|
||
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 dark:text-gray-400 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>
|
||
}
|
||
/>
|
||
</List.Item>
|
||
)}
|
||
/>
|
||
</InfiniteScroll>
|
||
</div>
|
||
);
|
||
|
||
const renderTypeTags = () => (
|
||
<div className="flex flex-wrap gap-2">
|
||
{Object.entries({ 全部: null, ...FILE_TYPES, 其他: null }).map(([type]) => (
|
||
<Tag.CheckableTag
|
||
key={type}
|
||
checked={selectedType === type}
|
||
onChange={(checked) => handleTypeChange(checked ? type : "全部")}
|
||
className={`cursor-pointer ${typeStats[type] === 0 ? 'opacity-50' : ''}`}
|
||
>
|
||
{`${type} (${typeStats[type] || 0})`}
|
||
</Tag.CheckableTag>
|
||
))}
|
||
</div>
|
||
);
|
||
return (
|
||
<div className="flex h-full ">
|
||
<div className="w-1/3 p-2 flex flex-col space-y-4 h-full overflow-auto ">
|
||
<div className="rounded-lg shadow-sm p-4 ">
|
||
<Dragger
|
||
{...uploadProps}
|
||
className={`px-6 py-8 group
|
||
${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||
disabled={isUploading}
|
||
>
|
||
<div className="space-y-3 text-center">
|
||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full group-hover:bg-blue-100 dark:group-hover:bg-blue-800 transition-colors">
|
||
{isUploading ? (
|
||
<LoadingSpinner />
|
||
) : (
|
||
<InboxOutlined className="text-3xl text-blue-500 dark:text-blue-400" />
|
||
)}
|
||
</div>
|
||
<div>
|
||
<p className="text-base font-medium text-gray-900 dark:text-gray-100">
|
||
{isUploading ? '正在上传...' : '点击或拖拽文件上传'}
|
||
</p>
|
||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||
支持单个或批量上传,文件大小不超过50MB
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</Dragger>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<Input
|
||
variant="filled"
|
||
placeholder="搜索文件名..."
|
||
allowClear
|
||
onChange={(e) => setSearchText(e.target.value)}
|
||
size="large"
|
||
/>
|
||
<div className=" p-3 rounded-lg shadow-sm">
|
||
{renderTypeTags()}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 文件列表 */}
|
||
{loading && displayFiles.length === 0 ? (
|
||
<div className="flex-1 flex items-center justify-center">
|
||
<LoadingSpinner />
|
||
</div>
|
||
) : (
|
||
renderFileList()
|
||
)}
|
||
</div>
|
||
|
||
{/* 右侧预览区域 */}
|
||
<div className="flex-1 p-4 border-l border-gray-200">
|
||
{selectedFile ? (
|
||
<>
|
||
<div className="mb-4 pb-4 border-b border-gray-200">
|
||
<Space size="middle" align="center">
|
||
<Space>
|
||
<span className="text-lg font-medium ">
|
||
{selectedFile.name}
|
||
</span>
|
||
<Button
|
||
type="text"
|
||
icon={<EditOutlined />}
|
||
onClick={startRename}
|
||
className="text-gray-500 dark:text-gray-100 hover:text-blue-500"
|
||
/>
|
||
</Space>
|
||
{isHtml(selectedFile) && (
|
||
<Switch
|
||
checkedChildren="预览"
|
||
unCheckedChildren="代码"
|
||
checked={isPreview}
|
||
onChange={setIsPreview}
|
||
/>
|
||
)}
|
||
{!isImage(selectedFile) && (
|
||
<Button
|
||
type="primary"
|
||
onClick={handleSaveContent}
|
||
className="bg-blue-500 hover:bg-blue-600"
|
||
>
|
||
保存
|
||
</Button>
|
||
)}
|
||
</Space>
|
||
</div>
|
||
|
||
<div className="mt-4">
|
||
{isImage(selectedFile) ? (
|
||
<div className="flex justify-center">
|
||
<Image
|
||
src={getFileUrl(selectedFile.name)}
|
||
alt={selectedFile.name}
|
||
className="max-w-full max-h-[80vh] object-contain rounded-lg"
|
||
loading="lazy"
|
||
preview={{
|
||
toolbarRender: () => null,
|
||
maskClassName: "backdrop-blur-sm",
|
||
}}
|
||
placeholder={
|
||
<div className="w-full h-[80vh] bg-gray-50 rounded-lg flex items-center justify-center">
|
||
<LoadingSpinner />
|
||
</div>
|
||
}
|
||
/>
|
||
</div>
|
||
) : isHtml(selectedFile) ? (
|
||
isPreview ? (
|
||
<iframe
|
||
srcDoc={fileContent}
|
||
className="w-full h-[calc(100vh-200px)] border rounded-lg"
|
||
/>
|
||
) : (
|
||
<MonacoEditor
|
||
height="calc(100vh - 200px)"
|
||
language="html"
|
||
theme="vs-light"
|
||
value={fileContent}
|
||
onChange={handleEditorChange}
|
||
options={{
|
||
minimap: { enabled: true },
|
||
roundedSelection: true,
|
||
padding: { top: 16 },
|
||
}}
|
||
className="rounded-lg overflow-hidden"
|
||
/>
|
||
)
|
||
) : (
|
||
<MonacoEditor
|
||
height="calc(100vh - 200px)"
|
||
language="plaintext"
|
||
theme="vs-light"
|
||
value={fileContent}
|
||
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>
|
||
|
||
{/* 重命名对话框 */}
|
||
<Modal
|
||
title="重命名文件"
|
||
open={isRenaming}
|
||
onOk={handleRename}
|
||
onCancel={() => setIsRenaming(false)}
|
||
okText="确认"
|
||
cancelText="取消"
|
||
>
|
||
<Input
|
||
value={newFileName}
|
||
onChange={(e) => setNewFileName(e.target.value)}
|
||
placeholder="请输入新文件名"
|
||
autoFocus
|
||
className="mt-4"
|
||
/>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default StorageManager;
|