报价单魔魁
This commit is contained in:
@@ -1,76 +1,121 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { supabase } from '@/config/supabase';
|
||||
import { Upload, Button, message, List, Switch, Space, Input, Tag, Pagination, Modal, Image, Popconfirm } from 'antd';
|
||||
import { UploadOutlined, FileTextOutlined, FileImageOutlined,
|
||||
FileMarkdownOutlined, FilePdfOutlined, FileWordOutlined,
|
||||
FileExcelOutlined, InboxOutlined, SearchOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import MonacoEditor from '@monaco-editor/react';
|
||||
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,
|
||||
} 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'],
|
||||
'其他': []
|
||||
图片: ["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 [fileContent, setFileContent] = useState("");
|
||||
const [isPreview, setIsPreview] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [selectedType, setSelectedType] = useState('全部');
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
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 [newFileName, setNewFileName] = useState("");
|
||||
|
||||
// 文件图标映射
|
||||
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 />,
|
||||
"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/')) {
|
||||
|
||||
if (mimetype?.startsWith("image/")) {
|
||||
return <FileImageOutlined />;
|
||||
}
|
||||
|
||||
|
||||
return iconMap[mimetype] || <FileTextOutlined />;
|
||||
};
|
||||
|
||||
// 获取所有文件
|
||||
const fetchAllFiles = async () => {
|
||||
const fetchAllFiles = async (isInitial = true) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data, error } = await supabase.storage
|
||||
.from('file')
|
||||
.list('', {
|
||||
sortBy: { column: 'created_at', order: 'desc' } // 按创建时间倒序
|
||||
.from("file")
|
||||
.list("", {
|
||||
limit: isInitial ? INITIAL_LOAD_SIZE : LOAD_MORE_SIZE,
|
||||
offset: isInitial ? 0 : displayFiles.length,
|
||||
sortBy: { column: "created_at", order: "desc" },
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
setAllFiles(data || []);
|
||||
if (isInitial) {
|
||||
setDisplayFiles(data || []);
|
||||
} else {
|
||||
setDisplayFiles(prev => [...prev, ...(data || [])]);
|
||||
}
|
||||
|
||||
|
||||
setHasMore(data?.length === (isInitial ? INITIAL_LOAD_SIZE : LOAD_MORE_SIZE));
|
||||
} catch (error) {
|
||||
console.error('获取文件列表错误:', error);
|
||||
message.error(`获取文件列表失败: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -79,50 +124,57 @@ const StorageManager = () => {
|
||||
|
||||
// 获取文件URL
|
||||
const getFileUrl = (fileName) => {
|
||||
const { data } = supabase.storage
|
||||
.from('file')
|
||||
.getPublicUrl(fileName);
|
||||
const { data } = supabase.storage.from("file").getPublicUrl(fileName);
|
||||
return data.publicUrl;
|
||||
};
|
||||
|
||||
// 预览文件
|
||||
const previewFile = async (file) => {
|
||||
try {
|
||||
// Handle PDF and other binary files
|
||||
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')
|
||||
.from("file")
|
||||
.download(file.name);
|
||||
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const content = await data.text();
|
||||
setSelectedFile(file);
|
||||
setFileContent(content);
|
||||
} catch (error) {
|
||||
message.error('文件预览失败');
|
||||
message.error("文件预览失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 上传文件配置
|
||||
const uploadProps = {
|
||||
name: 'file',
|
||||
name: "file",
|
||||
multiple: true,
|
||||
showUploadList: false,
|
||||
customRequest: async ({ file, onSuccess, onError }) => {
|
||||
try {
|
||||
const fileName = file.name;
|
||||
// 检查文件是否已存在
|
||||
const fileExists = allFiles.some(f => f.name === fileName);
|
||||
|
||||
// 检查文件是否存在
|
||||
const fileExists = allFiles.some((f) => f.name === fileName);
|
||||
|
||||
if (fileExists) {
|
||||
throw new Error('文件已存在');
|
||||
throw new Error("文件已存在");
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from('file')
|
||||
.from("file")
|
||||
.upload(fileName, file);
|
||||
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
|
||||
message.success(`${fileName} 上传成功`);
|
||||
onSuccess(data);
|
||||
fetchAllFiles();
|
||||
@@ -134,7 +186,7 @@ const StorageManager = () => {
|
||||
beforeUpload: (file) => {
|
||||
const isLt50M = file.size / 1024 / 1024 < 50;
|
||||
if (!isLt50M) {
|
||||
message.error('文件必须小于 50MB!');
|
||||
message.error("文件必须小于 50MB!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -143,40 +195,55 @@ const StorageManager = () => {
|
||||
|
||||
// 文件过滤逻辑
|
||||
const filteredFiles = useMemo(() => {
|
||||
return allFiles.filter(file => {
|
||||
const matchesSearch = file.name.toLowerCase().includes(searchText.toLowerCase());
|
||||
if (selectedType === '全部') return matchesSearch;
|
||||
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]?.includes(mimetype);
|
||||
|
||||
return matchesSearch && matchesType;
|
||||
});
|
||||
}, [allFiles, searchText, selectedType]);
|
||||
}, [displayFiles, searchText, selectedType]);
|
||||
|
||||
// 当前页的文件
|
||||
const currentPageFiles = useMemo(() => {
|
||||
const { current, pageSize } = pagination;
|
||||
const start = (current - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
return filteredFiles.slice(start, end);
|
||||
}, [filteredFiles, pagination]);
|
||||
|
||||
|
||||
// 获取文件类型统计
|
||||
const typeStats = useMemo(() => {
|
||||
const stats = { '全部': allFiles.length };
|
||||
Object.entries(FILE_TYPES).forEach(([type, mimetypes]) => {
|
||||
stats[type] = allFiles.filter(file => {
|
||||
const mimetype = file.metadata?.mimetype;
|
||||
return mimetypes.includes(mimetype);
|
||||
}).length;
|
||||
const stats = { 全部: displayFiles.length };
|
||||
|
||||
// 初始化所有类型的计数为0
|
||||
Object.keys(FILE_TYPES).forEach(type => {
|
||||
stats[type] = 0;
|
||||
});
|
||||
return stats;
|
||||
}, [allFiles]);
|
||||
|
||||
// 处理分页变化
|
||||
const handlePageChange = (page, pageSize) => {
|
||||
setPagination({ ...pagination, current: page, pageSize });
|
||||
};
|
||||
// 统计每个文件的类型
|
||||
displayFiles.forEach((file) => {
|
||||
const mimetype = file.metadata?.mimetype;
|
||||
let counted = false;
|
||||
|
||||
// 遍历所有文件类型配置
|
||||
Object.entries(FILE_TYPES).forEach(([type, mimetypes]) => {
|
||||
if (mimetypes.includes(mimetype)) {
|
||||
stats[type]++;
|
||||
counted = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果文件类型不在预定义类型中,归类为"其他"
|
||||
if (!counted) {
|
||||
stats['其他']++;
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}, [displayFiles]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllFiles();
|
||||
@@ -184,34 +251,34 @@ const StorageManager = () => {
|
||||
|
||||
// 判断是否是图片
|
||||
const isImage = (file) => {
|
||||
return file.metadata?.mimetype?.startsWith('image/');
|
||||
return file.metadata?.mimetype?.startsWith("image/");
|
||||
};
|
||||
|
||||
// 判断是否是HTML
|
||||
const isHtml = (file) => {
|
||||
return file.metadata?.mimetype === 'text/html';
|
||||
return file.metadata?.mimetype === "text/html";
|
||||
};
|
||||
|
||||
// 保存文件内容
|
||||
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 blob = new Blob([fileContent], { type: "text/plain" });
|
||||
const file = new File([blob], selectedFile.name, { type: "text/plain" });
|
||||
|
||||
// 上传更新后的文件
|
||||
// 上<EFBFBD><EFBFBD>更新后的文件
|
||||
const { error } = await supabase.storage
|
||||
.from('file')
|
||||
.from("file")
|
||||
.update(selectedFile.name, file);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
message.success('保存成功');
|
||||
message.success("保存成功");
|
||||
fetchAllFiles(); // 刷新文件列表
|
||||
} catch (error) {
|
||||
console.error('保存文件错误:', error);
|
||||
console.error("保存文件错误:", error);
|
||||
message.error(`保存失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
@@ -224,41 +291,41 @@ const StorageManager = () => {
|
||||
// 处理重命名
|
||||
const handleRename = async () => {
|
||||
if (!selectedFile || !newFileName) return;
|
||||
|
||||
|
||||
try {
|
||||
// 检查新文件名是否已存在
|
||||
const fileExists = allFiles.some(f => f.name === newFileName);
|
||||
const fileExists = allFiles.some((f) => f.name === newFileName);
|
||||
if (fileExists) {
|
||||
throw new Error('文件名已存在');
|
||||
throw new Error("文件名已存在");
|
||||
}
|
||||
|
||||
// 获取原文件内容
|
||||
const { data: fileData, error: downloadError } = await supabase.storage
|
||||
.from('file')
|
||||
.from("file")
|
||||
.download(selectedFile.name);
|
||||
|
||||
|
||||
if (downloadError) throw downloadError;
|
||||
|
||||
// 创建新文件
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from('file')
|
||||
.from("file")
|
||||
.upload(newFileName, fileData);
|
||||
|
||||
|
||||
if (uploadError) throw uploadError;
|
||||
|
||||
// 删除旧文件
|
||||
const { error: deleteError } = await supabase.storage
|
||||
.from('file')
|
||||
.from("file")
|
||||
.remove([selectedFile.name]);
|
||||
|
||||
|
||||
if (deleteError) throw deleteError;
|
||||
|
||||
message.success('重命名成功');
|
||||
message.success("重命名成功");
|
||||
setIsRenaming(false);
|
||||
setSelectedFile({ ...selectedFile, name: newFileName });
|
||||
fetchAllFiles();
|
||||
} catch (error) {
|
||||
console.error('重命名错误:', error);
|
||||
console.error("重命名错误:", error);
|
||||
message.error(`重命名失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
@@ -268,37 +335,169 @@ const StorageManager = () => {
|
||||
setNewFileName(selectedFile.name);
|
||||
setIsRenaming(true);
|
||||
};
|
||||
// 添加文件删除功能
|
||||
const handleDelete = async (fileName) => {
|
||||
// 添加文件删除功能
|
||||
const handleDelete = async (fileName) => {
|
||||
try {
|
||||
const { error } = await supabase.storage
|
||||
.from('file')
|
||||
.remove([fileName]);
|
||||
|
||||
const { error } = await supabase.storage.from("file").remove([fileName]);
|
||||
if (error) throw error;
|
||||
|
||||
message.success('文件删除成功');
|
||||
message.success("文件删除成功");
|
||||
fetchAllFiles();
|
||||
if (selectedFile?.name === fileName) {
|
||||
setSelectedFile(null);
|
||||
setFileContent('');
|
||||
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 handleSearch = (value) => {
|
||||
setSearchText(value);
|
||||
// 不需要重新调用 fetchAllFiles,因为搜索是在前端过滤
|
||||
};
|
||||
|
||||
const handleTypeChange = (type) => {
|
||||
setSelectedType(type);
|
||||
// 不需要重新调用 fetchAllFiles,因为类型筛选是在前端过滤
|
||||
};
|
||||
|
||||
// 渲染文件列表
|
||||
const renderFileList = () => (
|
||||
<div className="flex-1 overflow-y-auto" id="scrollableDiv">
|
||||
<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>
|
||||
}
|
||||
scrollableTarget="scrollableDiv"
|
||||
endMessage={
|
||||
<p className="text-center text-gray-500 py-4">
|
||||
{displayFiles.length > 0
|
||||
? hasFilters
|
||||
? "已显示所有匹配文件"
|
||||
: "已加载全部文件"
|
||||
: "暂无文件"}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<List
|
||||
loading={loading && displayFiles.length === 0}
|
||||
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>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
isImage(file) ? (
|
||||
<Image
|
||||
src={getFileUrl(file.name)}
|
||||
alt={file.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="object-cover rounded"
|
||||
preview={false}
|
||||
loading="lazy"
|
||||
placeholder={
|
||||
<div className="w-8 h-8 bg-gray-200 rounded animate-pulse" />
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
getFileIcon(file)
|
||||
)
|
||||
}
|
||||
title={file.name}
|
||||
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>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 渲染文件类型标签
|
||||
const renderTypeTags = () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries({ 全部: null, ...FILE_TYPES }).map(([type]) => (
|
||||
<Tag.CheckableTag
|
||||
key={type}
|
||||
checked={selectedType === type}
|
||||
onChange={(checked) => handleTypeChange(checked ? type : "全部")}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{`${type} (${typeStats[type] || 0})`}
|
||||
</Tag.CheckableTag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50">
|
||||
{/* 左侧文件列表 */}
|
||||
<div className="w-1/3 p-4 border-r border-gray-200 flex flex-col">
|
||||
{/* 上传区域 */}
|
||||
<div className="mb-4">
|
||||
<Dragger {...uploadProps} className="bg-white p-4 rounded-lg shadow-sm">
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或者拖拽文件到此区域上传</p>
|
||||
<div className="w-1/3 p-2 border-r border-gray-200 flex flex-col">
|
||||
<div className="mb-4 h-[150px]">
|
||||
<Dragger
|
||||
{...uploadProps}
|
||||
className="bg-white p-2 rounded-lg shadow-sm"
|
||||
>
|
||||
<p className="text-base">点击或者拖拽文件到此区域上传</p>
|
||||
<p className="ant-upload-hint text-xs text-gray-500">
|
||||
支持单个或批量上传,文件大小不超过50MB
|
||||
</p>
|
||||
@@ -310,112 +509,17 @@ const handleDelete = async (fileName) => {
|
||||
<Search
|
||||
placeholder="搜索文件名..."
|
||||
allowClear
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
}}
|
||||
className="w-full"
|
||||
size="large"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries({ '全部': null, ...FILE_TYPES }).map(([type]) => (
|
||||
<Tag.CheckableTag
|
||||
key={type}
|
||||
checked={selectedType === type}
|
||||
onChange={checked => {
|
||||
setSelectedType(checked ? type : '全部');
|
||||
setPagination(prev => ({ ...prev, current: 1 }));
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{type} ({typeStats[type] || 0})
|
||||
</Tag.CheckableTag>
|
||||
))}
|
||||
</div>
|
||||
{renderTypeTags()}
|
||||
</div>
|
||||
|
||||
{/* 文件列表 */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<List
|
||||
loading={loading}
|
||||
dataSource={searchText ? filteredFiles : currentPageFiles}
|
||||
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>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
isImage(file) ? (
|
||||
<Image
|
||||
src={getFileUrl(file.name)}
|
||||
alt={file.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="object-cover rounded"
|
||||
preview={false}
|
||||
loading="lazy"
|
||||
placeholder={
|
||||
<div className="w-8 h-8 bg-gray-200 rounded animate-pulse" />
|
||||
}
|
||||
/>
|
||||
) : getFileIcon(file)
|
||||
}
|
||||
title={file.name}
|
||||
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>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* 分页器 - 只<><E58FAA>非搜索状态下显示 */}
|
||||
{!searchText && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Pagination
|
||||
current={pagination.current}
|
||||
pageSize={pagination.pageSize}
|
||||
total={filteredFiles.length}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total) => `共 ${total} 个文件`}
|
||||
pageSizeOptions={['10', '20', '50', '100', '200']}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderFileList()}
|
||||
</div>
|
||||
|
||||
{/* 右侧预览区域 */}
|
||||
@@ -428,7 +532,7 @@ const handleDelete = async (fileName) => {
|
||||
<span className="font-medium text-lg">
|
||||
{selectedFile.name}
|
||||
</span>
|
||||
<Button
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={startRename}
|
||||
@@ -443,27 +547,24 @@ const handleDelete = async (fileName) => {
|
||||
/>
|
||||
)}
|
||||
{!isImage(selectedFile) && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSaveContent}
|
||||
>
|
||||
<Button type="primary" onClick={handleSaveContent}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-4">
|
||||
{isImage(selectedFile) ? (
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src={getFileUrl(selectedFile.name)}
|
||||
<Image
|
||||
src={getFileUrl(selectedFile.name)}
|
||||
alt={selectedFile.name}
|
||||
className="max-w-full max-h-[80vh] object-contain"
|
||||
loading="lazy"
|
||||
preview={{
|
||||
preview={{
|
||||
toolbarRender: () => null, // 隐藏底部工具栏
|
||||
maskClassName: 'backdrop-blur-sm'
|
||||
maskClassName: "backdrop-blur-sm",
|
||||
}}
|
||||
placeholder={
|
||||
<div className="w-full h-[80vh] bg-gray-100 rounded flex items-center justify-center">
|
||||
@@ -486,7 +587,7 @@ const handleDelete = async (fileName) => {
|
||||
value={fileContent}
|
||||
onChange={handleEditorChange}
|
||||
options={{
|
||||
minimap: { enabled: true }
|
||||
minimap: { enabled: true },
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -498,7 +599,7 @@ const handleDelete = async (fileName) => {
|
||||
value={fileContent}
|
||||
onChange={handleEditorChange}
|
||||
options={{
|
||||
minimap: { enabled: false }
|
||||
minimap: { enabled: false },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -522,7 +623,7 @@ const handleDelete = async (fileName) => {
|
||||
>
|
||||
<Input
|
||||
value={newFileName}
|
||||
onChange={e => setNewFileName(e.target.value)}
|
||||
onChange={(e) => setNewFileName(e.target.value)}
|
||||
placeholder="请输入新文件名"
|
||||
autoFocus
|
||||
/>
|
||||
@@ -531,5 +632,4 @@ const handleDelete = async (fileName) => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default StorageManager;
|
||||
export default StorageManager;
|
||||
|
||||
Reference in New Issue
Block a user