储存桶file文件的增删改查
This commit is contained in:
13
package.json
13
package.json
@@ -10,24 +10,25 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.2.6",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"@supabase/supabase-js": "^2.38.4",
|
||||||
|
"antd": "^5.11.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"antd": "^5.11.0",
|
|
||||||
"@ant-design/icons": "^5.2.6",
|
|
||||||
"react-router-dom": "^6.18.0",
|
"react-router-dom": "^6.18.0",
|
||||||
"@supabase/supabase-js": "^2.38.4",
|
"recharts": "^2.9.0",
|
||||||
"styled-components": "^6.1.0",
|
"styled-components": "^6.1.0"
|
||||||
"recharts": "^2.9.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@vitejs/plugin-react": "^4.0.3",
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
"eslint": "^8.45.0",
|
"eslint": "^8.45.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.3",
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
"autoprefixer": "^10.4.16",
|
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.69.5",
|
||||||
"tailwindcss": "^3.3.5",
|
"tailwindcss": "^3.3.5",
|
||||||
|
|||||||
37
pnpm-lock.yaml
generated
37
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@ant-design/icons':
|
'@ant-design/icons':
|
||||||
specifier: ^5.2.6
|
specifier: ^5.2.6
|
||||||
version: 5.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 5.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@monaco-editor/react':
|
||||||
|
specifier: ^4.6.0
|
||||||
|
version: 4.6.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@supabase/supabase-js':
|
'@supabase/supabase-js':
|
||||||
specifier: ^2.38.4
|
specifier: ^2.38.4
|
||||||
version: 2.47.7
|
version: 2.47.7
|
||||||
@@ -401,6 +404,18 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||||
|
|
||||||
|
'@monaco-editor/loader@1.4.0':
|
||||||
|
resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==}
|
||||||
|
peerDependencies:
|
||||||
|
monaco-editor: '>= 0.21.0 < 1'
|
||||||
|
|
||||||
|
'@monaco-editor/react@4.6.0':
|
||||||
|
resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==}
|
||||||
|
peerDependencies:
|
||||||
|
monaco-editor: '>= 0.25.0 < 1'
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1454,6 +1469,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
|
monaco-editor@0.52.2:
|
||||||
|
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -2035,6 +2053,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
state-local@1.0.7:
|
||||||
|
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
|
||||||
|
|
||||||
string-convert@0.2.1:
|
string-convert@0.2.1:
|
||||||
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
|
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
|
||||||
|
|
||||||
@@ -2593,6 +2614,18 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
'@monaco-editor/loader@1.4.0(monaco-editor@0.52.2)':
|
||||||
|
dependencies:
|
||||||
|
monaco-editor: 0.52.2
|
||||||
|
state-local: 1.0.7
|
||||||
|
|
||||||
|
'@monaco-editor/react@4.6.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@monaco-editor/loader': 1.4.0(monaco-editor@0.52.2)
|
||||||
|
monaco-editor: 0.52.2
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@@ -3850,6 +3883,8 @@ snapshots:
|
|||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
|
monaco-editor@0.52.2: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
mz@2.7.0:
|
mz@2.7.0:
|
||||||
@@ -4538,6 +4573,8 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
state-local@1.0.7: {}
|
||||||
|
|
||||||
string-convert@0.2.1: {}
|
string-convert@0.2.1: {}
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ const resourceRoutes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bucket',
|
path: 'bucket',
|
||||||
component: lazy(() => import('@/pages/resource/team')),
|
component: lazy(() => import('@/pages/resource/bucket')),
|
||||||
name: '对象存储',
|
name: '对象存储',
|
||||||
icon: 'team',
|
icon: 'shop',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
528
src/pages/resource/bucket/index.jsx
Normal file
528
src/pages/resource/bucket/index.jsx
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { supabase } from '@/config/supabase';
|
||||||
|
import { Card, Upload, Button, message, List, Switch, Space, Input, Select, Tag, Pagination, Modal, Image } from 'antd';
|
||||||
|
import { UploadOutlined, FileTextOutlined, FileImageOutlined,
|
||||||
|
FileMarkdownOutlined, FilePdfOutlined, FileWordOutlined,
|
||||||
|
FileExcelOutlined, InboxOutlined, SearchOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
|
import MonacoEditor from '@monaco-editor/react';
|
||||||
|
|
||||||
|
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 [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
});
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
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 />,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mimetype?.startsWith('image/')) {
|
||||||
|
return <FileImageOutlined />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconMap[mimetype] || <FileTextOutlined />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有文件
|
||||||
|
const fetchAllFiles = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase.storage
|
||||||
|
.from('file')
|
||||||
|
.list('', {
|
||||||
|
sortBy: { column: 'created_at', order: 'desc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setAllFiles(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文件列表错误:', 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 {
|
||||||
|
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 }) => {
|
||||||
|
try {
|
||||||
|
const fileName = file.name;
|
||||||
|
// 检查文件是否已存在
|
||||||
|
const fileExists = allFiles.some(f => f.name === fileName);
|
||||||
|
|
||||||
|
if (fileExists) {
|
||||||
|
throw new Error('文件已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await supabase.storage
|
||||||
|
.from('file')
|
||||||
|
.upload(fileName, file);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
message.success(`${fileName} 上传成功`);
|
||||||
|
onSuccess(data);
|
||||||
|
fetchAllFiles();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`${file.name} 上传失败: ${error.message}`);
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUpload: (file) => {
|
||||||
|
const isLt50M = file.size / 1024 / 1024 < 50;
|
||||||
|
if (!isLt50M) {
|
||||||
|
message.error('文件必须小于 50MB!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 文件过滤逻辑
|
||||||
|
const filteredFiles = useMemo(() => {
|
||||||
|
return allFiles.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]);
|
||||||
|
|
||||||
|
// 当前页的文件
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
return stats;
|
||||||
|
}, [allFiles]);
|
||||||
|
|
||||||
|
// 处理分页变化
|
||||||
|
const handlePageChange = (page, pageSize) => {
|
||||||
|
setPagination({ ...pagination, current: page, pageSize });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllFiles();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 判断是否是图片
|
||||||
|
const isImage = (file) => {
|
||||||
|
return file.metadata?.mimetype?.startsWith('image/');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断是否是HTML
|
||||||
|
const isHtml = (file) => {
|
||||||
|
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 { 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('文件删除成功');
|
||||||
|
fetchAllFiles();
|
||||||
|
if (selectedFile?.name === fileName) {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setFileContent('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`删除失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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">点击<EFBFBD><EFBFBD>拖拽文件到此区域上传</p>
|
||||||
|
<p className="ant-upload-hint text-xs text-gray-500">
|
||||||
|
支持单个或批量上传,文件大小不超过50MB
|
||||||
|
</p>
|
||||||
|
</Dragger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 搜索和筛选区域 */}
|
||||||
|
<div className="mb-4 space-y-3">
|
||||||
|
<Search
|
||||||
|
placeholder="搜索文件名..."
|
||||||
|
allowClear
|
||||||
|
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>
|
||||||
|
</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={[
|
||||||
|
<Button
|
||||||
|
key="delete"
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(file.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 分页器 - 只在非搜索状态下显示 */}
|
||||||
|
{!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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧预览区域 */}
|
||||||
|
<div className="flex-1 p-4 overflow-y-auto bg-white">
|
||||||
|
{selectedFile ? (
|
||||||
|
<>
|
||||||
|
<div className="mb-4 border-b pb-4">
|
||||||
|
<Space size="middle" align="center">
|
||||||
|
<Space>
|
||||||
|
<span className="font-medium text-lg">
|
||||||
|
{selectedFile.name}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={startRename}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
{isHtml(selectedFile) && (
|
||||||
|
<Switch
|
||||||
|
checkedChildren="预览"
|
||||||
|
unCheckedChildren="代码"
|
||||||
|
checked={isPreview}
|
||||||
|
onChange={setIsPreview}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isImage(selectedFile) && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSaveContent}
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</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"
|
||||||
|
loading="lazy"
|
||||||
|
preview={{
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : isHtml(selectedFile) ? (
|
||||||
|
isPreview ? (
|
||||||
|
<iframe
|
||||||
|
srcDoc={fileContent}
|
||||||
|
className="w-full h-[calc(100vh-200px)] border rounded"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MonacoEditor
|
||||||
|
height="calc(100vh - 200px)"
|
||||||
|
language="html"
|
||||||
|
theme="vs-light"
|
||||||
|
value={fileContent}
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: true }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<MonacoEditor
|
||||||
|
height="calc(100vh - 200px)"
|
||||||
|
language="plaintext"
|
||||||
|
theme="vs-light"
|
||||||
|
value={fileContent}
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="h-full flex items-center justify-center text-gray-400">
|
||||||
|
选择文件以预览
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 重命名对话框 */}
|
||||||
|
<Modal
|
||||||
|
title="重命名文件"
|
||||||
|
open={isRenaming}
|
||||||
|
onOk={handleRename}
|
||||||
|
onCancel={() => setIsRenaming(false)}
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={newFileName}
|
||||||
|
onChange={e => setNewFileName(e.target.value)}
|
||||||
|
placeholder="请输入新文件名"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default StorageManager;
|
||||||
Reference in New Issue
Block a user