From 550032d1dcd59654b54433414fb132df85aa22e3 Mon Sep 17 00:00:00 2001 From: liamzi Date: Mon, 16 Dec 2024 13:38:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=82=A8=E5=AD=98=E6=A1=B6file=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=9A=84=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 13 +- pnpm-lock.yaml | 37 ++ src/config/routes.js | 4 +- src/pages/resource/bucket/index.jsx | 528 ++++++++++++++++++++++++++++ 4 files changed, 574 insertions(+), 8 deletions(-) create mode 100644 src/pages/resource/bucket/index.jsx diff --git a/package.json b/package.json index 641778e..0eb6789 100644 --- a/package.json +++ b/package.json @@ -10,24 +10,25 @@ "preview": "vite preview" }, "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-dom": "^18.2.0", - "antd": "^5.11.0", - "@ant-design/icons": "^5.2.6", "react-router-dom": "^6.18.0", - "@supabase/supabase-js": "^2.38.4", - "styled-components": "^6.1.0", - "recharts": "^2.9.0" + "recharts": "^2.9.0", + "styled-components": "^6.1.0" }, "devDependencies": { "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", + "autoprefixer": "^10.4.16", "eslint": "^8.45.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", - "autoprefixer": "^10.4.16", "postcss": "^8.4.31", "sass": "^1.69.5", "tailwindcss": "^3.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc78b56..c131343 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@ant-design/icons': specifier: ^5.2.6 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': specifier: ^2.38.4 version: 2.47.7 @@ -401,6 +404,18 @@ packages: '@jridgewell/trace-mapping@0.3.25': 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': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1454,6 +1469,9 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2035,6 +2053,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} @@ -2593,6 +2614,18 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@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': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3850,6 +3883,8 @@ snapshots: minipass@7.1.2: {} + monaco-editor@0.52.2: {} + ms@2.1.3: {} mz@2.7.0: @@ -4538,6 +4573,8 @@ snapshots: source-map-js@1.2.1: {} + state-local@1.0.7: {} + string-convert@0.2.1: {} string-width@4.2.3: diff --git a/src/config/routes.js b/src/config/routes.js index 1df0c35..25332c4 100644 --- a/src/config/routes.js +++ b/src/config/routes.js @@ -18,9 +18,9 @@ const resourceRoutes = [ }, { path: 'bucket', - component: lazy(() => import('@/pages/resource/team')), + component: lazy(() => import('@/pages/resource/bucket')), name: '对象存储', - icon: 'team', + icon: 'shop', }, ]; diff --git a/src/pages/resource/bucket/index.jsx b/src/pages/resource/bucket/index.jsx new file mode 100644 index 0000000..34e7310 --- /dev/null +++ b/src/pages/resource/bucket/index.jsx @@ -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': , + '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 () => { + 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 ( +
+ {/* 左侧文件列表 */} +
+ {/* 上传区域 */} +
+ +

+ +

+

点击��拖拽文件到此区域上传

+

+ 支持单个或批量上传,文件大小不超过50MB +

+
+
+ + {/* 搜索和筛选区域 */} +
+ { + setSearchText(e.target.value); + setPagination(prev => ({ ...prev, current: 1 })); + }} + className="w-full" + size="large" + /> +
+ {Object.entries({ '全部': null, ...FILE_TYPES }).map(([type]) => ( + { + setSelectedType(checked ? type : '全部'); + setPagination(prev => ({ ...prev, current: 1 })); + }} + className="cursor-pointer" + > + {type} ({typeStats[type] || 0}) + + ))} +
+
+ + {/* 文件列表 */} +
+ ( + previewFile(file)} + actions={[ + + ]} + > + + } + /> + ) : getFileIcon(file) + } + title={file.name} + description={ +
+ 类型: {file.metadata?.mimetype} + 大小: {(file.metadata?.size / 1024).toFixed(2)} KB + + 创建时间: {new Date(file.created_at).toLocaleString()} + +
+ } + /> +
+ )} + /> + + {/* 分页器 - 只在非搜索状态下显示 */} + {!searchText && ( +
+ `共 ${total} 个文件`} + pageSizeOptions={['10', '20', '50', '100', '200']} + /> +
+ )} +
+
+ + {/* 右侧预览区域 */} +
+ {selectedFile ? ( + <> +
+ + + + {selectedFile.name} + + + )} + +
+ +
+ {isImage(selectedFile) ? ( +
+ {selectedFile.name} null, // 隐藏底部工具栏 + maskClassName: 'backdrop-blur-sm' + }} + placeholder={ +
+
+
+ } + /> +
+ ) : isHtml(selectedFile) ? ( + isPreview ? ( +