'use client' export const runtime = 'edge' import { useEffect, useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import * as z from "zod" import { Card, CardContent } from "@/registry/new-york/ui/card" import { Input } from "@/registry/new-york/ui/input" import { Button } from "@/registry/new-york/ui/button" import { useToast } from "@/registry/new-york/hooks/use-toast" import { Loader2, Plus, Upload, X, AlertCircle, Copy, Inbox, Search, Trash2, CheckSquare, Square } from "lucide-react" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/registry/new-york/ui/dialog" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/registry/new-york/ui/form" import { Toaster } from "@/registry/new-york/ui/toaster" const Icons = { loader2: Loader2, plus: Plus, upload: Upload, x: X, alertCircle: AlertCircle, copy: Copy, inbox: Inbox, search: Search, trash2: Trash2, checkSquare: CheckSquare, square: Square } const formSchema = z.object({ resource: z.object({ image: z.string().optional(), }), }) // 假设 ResourceMetadataItem 是 metadata 中每个项的类型 interface ResourceMetadataItem { hash: string; path: string; // 根据实际结构添加其他属性 } // Rename the local declaration interface LocalResourceMetadata { id: string; title: string; items: Array<{ title: string; description: string; icon: string; url: string; }>; } // 修改骨架屏组件,进一步调整大小 const ResourceGridSkeleton = () => (
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((index) => (
))}
); // Add style tag to the document if (typeof document !== 'undefined') { const styleSheet = document.createElement('style'); styleSheet.textContent = ` @keyframes shimmer { 0% { transform: translateX(-100%) skewX(-12deg); } 100% { transform: translateX(200%) skewX(-12deg); } } `; document.head.appendChild(styleSheet); } export default function ResourceManagement() { const { toast } = useToast() const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { resource: { image: undefined, }, }, }) const [resources, setResources] = useState([]); const [error, setError] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); // State to control dialog visibility const [uploadProgress, setUploadProgress] = useState(0); // State to track upload progress const [isUploading, setIsUploading] = useState(false); // State to track if an upload is in progress const [selectedImage, setSelectedImage] = useState(null); // State to hold the selected image for preview const [uploadSpeed, setUploadSpeed] = useState(0); const [abortController, setAbortController] = useState(null); const [isLoading, setIsLoading] = useState(true); const [selectedFile, setSelectedFile] = useState(null); // 新增文件状态 const [searchQuery, setSearchQuery] = useState(""); const [selectedResources, setSelectedResources] = useState>(new Set()); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [resourceReferences, setResourceReferences] = useState>>({}); const fetchResources = async () => { try { setIsLoading(true); const response = await fetch('/api/resource'); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); if (data.metadata && Array.isArray(data.metadata)) { const transformedResources: LocalResourceMetadata[] = data.metadata.map((item: ResourceMetadataItem, index: number) => ({ id: item.hash, title: `Resource ${index + 1}`, items: [{ title: item.path, description: '', icon: '', url: item.path.startsWith('/') ? item.path : `/${item.path}` }] })); setResources(transformedResources); } else { throw new Error('Metadata is not available or is not an array'); } } catch (error) { if (error instanceof Error) { setError(error.message); } else { setError('An unknown error occurred'); } } finally { setIsLoading(false); } }; // Update useEffect to use the new fetchResources function useEffect(() => { fetchResources(); }, []); async function onSubmit() { try { if (!selectedFile) { toast({ title: "错误", description: "请先选择图片", variant: "destructive", }); return; } if (selectedImage) { await uploadImageWithProgress(selectedImage); // Refresh the resources list after successful upload await fetchResources(); } toast({ title: "成功", description: "资源已上传", }); setIsDialogOpen(false); form.reset(); setSelectedImage(null); setSelectedFile(null); } catch (error) { if (error instanceof Error) { toast({ title: "错误", description: error.message || "上传资源失败", variant: "destructive", }); } else { toast({ title: "错误", description: "上传资源失败", variant: "destructive", }); } } } const handleImageChange = async (file: File) => { // Check file size (5MB limit) if (file.size > 5 * 1024 * 1024) { toast({ title: "错误", description: "文件大小不能超过 5MB", variant: "destructive", }); return; } // Check file type if (!file.type.startsWith('image/')) { toast({ title: "错误", description: "请选择有效的图片文件", variant: "destructive", }); return; } setSelectedFile(file); // 保存文件对象 const base64Image = await toBase64(file); setSelectedImage(base64Image); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); const file = e.dataTransfer.files[0]; if (file) { handleImageChange(file); } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }; const uploadImageWithProgress = (base64Image: string) => { return new Promise((resolve, reject) => { const controller = new AbortController(); setAbortController(controller); const xhr = new XMLHttpRequest(); xhr.open("POST", "/api/resource", true); xhr.setRequestHeader("Content-Type", "application/json"); let startTime = Date.now(); xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const percentComplete = (Number(event.loaded) / Number(event.total)) * 100; setUploadProgress(Math.round(percentComplete)); setIsUploading(true); // Calculate upload speed (bytes per second) const currentTime = Date.now(); const elapsedTime = (currentTime - startTime) / 1000; // Convert to seconds const speed = event.loaded / elapsedTime; // bytes per second setUploadSpeed(speed); } }; xhr.onload = () => { setIsUploading(false); setUploadProgress(0); setUploadSpeed(0); setAbortController(null); if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.response); resolve(response); } catch (e) { resolve(xhr.response); } } else { reject(new Error(`上传失败: ${xhr.statusText}`)); } }; xhr.onerror = () => { setIsUploading(false); setUploadProgress(0); setUploadSpeed(0); setAbortController(null); reject(new Error("网络错误,上传失败")); }; xhr.onabort = () => { setIsUploading(false); setUploadProgress(0); setUploadSpeed(0); setAbortController(null); toast({ title: "已取消", description: "上传已取消", }); }; xhr.send(JSON.stringify({ image: base64Image })); }); }; // 添加取消上传函数 const cancelUpload = () => { if (abortController) { abortController.abort(); } }; // Helper function to convert file to Base64 const toBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { resolve(reader.result as string); // Return Base64 string }; reader.onerror = (error) => reject(error); }); }; const copyToClipboard = (url: string) => { navigator.clipboard.writeText(url).then(() => { toast({ title: "成功", description: "链接已复制到剪贴板", }); }).catch((error) => { toast({ title: "错误", description: "复制链接失败", variant: "destructive", }); }); }; // 添加搜索过滤函数 const filteredResources = resources.filter((resource) => resource.items[0].url.toLowerCase().includes(searchQuery.toLowerCase()) ); // 批量选择相关函数 const toggleResourceSelection = (resourceId: string) => { const newSelected = new Set(selectedResources); if (newSelected.has(resourceId)) { newSelected.delete(resourceId); } else { newSelected.add(resourceId); } setSelectedResources(newSelected); }; const selectAllResources = () => { if (selectedResources.size === filteredResources.length) { setSelectedResources(new Set()); } else { setSelectedResources(new Set(filteredResources.map(r => r.id))); } }; // 检查资源引用 const checkResourceReferences = async (resourcePaths: string[]) => { try { const response = await fetch('/api/resource/check-references', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ resourcePaths }), }); if (!response.ok) { throw new Error('Failed to check references'); } const data = await response.json(); return data.references; } catch (error) { console.error('Error checking references:', error); return {}; } }; // 批量删除资源 const handleBatchDelete = async () => { if (selectedResources.size === 0) return; // 获取选中资源的路径和hash const selectedResourcesData = filteredResources.filter(r => selectedResources.has(r.id)); const resourcePaths = selectedResourcesData.map(r => r.items[0].url); const resourceHashes = selectedResourcesData.map(r => r.id); // 检查引用 const references = await checkResourceReferences(resourcePaths); setResourceReferences(references); setIsDeleteDialogOpen(true); }; // 确认删除 const confirmDelete = async () => { if (selectedResources.size === 0) return; try { setIsDeleting(true); const resourceHashes = Array.from(selectedResources); const response = await fetch('/api/resource', { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ resourceHashes }), }); if (!response.ok) { throw new Error('Failed to delete resources'); } const result = await response.json(); toast({ title: "成功", description: result.message || `成功删除 ${selectedResources.size} 个资源`, }); // 刷新资源列表 await fetchResources(); setSelectedResources(new Set()); setIsDeleteDialogOpen(false); } catch (error) { toast({ title: "错误", description: error instanceof Error ? error.message : "删除资源失败", variant: "destructive", }); } finally { setIsDeleting(false); } }; return ( <> {/* 顶部进度条 - 优化样式 */} {isUploading && (
正在上传...
{uploadProgress}% {(uploadSpeed / (1024 * 1024)).toFixed(2)} MB/s
{/* 背景动画效果 */}
{/* 主进度条 */}
{/* 光晕效果 */}
)}
setSearchQuery(e.target.value)} className="pl-10 max-w-md" />
{filteredResources.length > 0 && (
{selectedResources.size > 0 && ( )}
)}
{/* 添加搜索框 */}
{error && (

{error}

)} {isLoading ? ( ) : filteredResources.length > 0 ? (
{filteredResources.map((resource, index) => (
{/* 选择框 */}
{`Resource {/* 图片遮罩层和预览按钮 - 进一步缩小 */}
{/* 文件名和上传时间 */}

{resource.items[0].url.split('/').pop()}

{/* 复制链接按钮 - 进一步缩小 */}
))}
) : (

{searchQuery ? '未找到匹配的资源' : '暂无资源'}

{!searchQuery && ( )}
)}
{/* Dialog for adding resources */} { setIsDialogOpen(open); if (!open) { // 关闭对话框时重置状态 setSelectedImage(null); setSelectedFile(null); form.reset(); } }}> 上传资源 请选择或拖拽图片文件到此处上传。
( 选择图片 { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = (e) => { const files = (e.target as HTMLInputElement).files; if (files && files[0]) { handleImageChange(files[0]); } }; input.click(); }} > {selectedImage ? (
Preview
) : ( <>

点击或拖拽图片到此处

支持 JPG、PNG 格式,最大 5MB

)}
选择图片后,点击下方按钮开始上传
)} />
{/* 批量删除确认对话框 */} 确认删除资源 您即将删除 {selectedResources.size} 个资源,此操作不可撤销。
{/* 显示有引用的资源警告 */} {Object.entries(resourceReferences).some(([_, refs]) => refs.length > 0) && (

以下资源正在被使用中:

{Object.entries(resourceReferences).map(([resourcePath, refs]) => refs.length > 0 && (

{resourcePath.split('/').pop()}

    {refs.slice(0, 3).map((ref, index) => (
  • • {ref.location}
  • ))} {refs.length > 3 && (
  • • 还有 {refs.length - 3} 个引用...
  • )}
) )}

删除这些资源可能会导致相关功能异常,请确认是否继续。

)} {/* 显示将要删除的资源列表 */}

将要删除的资源 ({selectedResources.size} 个):

{selectedResources.size > 12 && (

显示前 12 个,共 {selectedResources.size} 个

)}
{selectedResources.size <= 12 ? ( // 少量资源时显示网格
{filteredResources .filter(r => selectedResources.has(r.id)) .map((resource, index) => (
Resource
{resource.items[0].url.split('/').pop()}
))}
) : ( // 大量资源时显示列表
{filteredResources .filter(r => selectedResources.has(r.id)) .slice(0, 12) .map((resource, index) => (
Resource

{resource.items[0].url.split('/').pop()}

{resource.items[0].url}

))} {selectedResources.size > 12 && (

还有 {selectedResources.size - 12} 个资源将被删除

)}
)}
) }