'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 && (
)}
{isLoading ? (
) : filteredResources.length > 0 ? (
{filteredResources.map((resource, index) => (
{/* 选择框 */}

{/* 图片遮罩层和预览按钮 - 进一步缩小 */}
{/* 文件名和上传时间 */}
{resource.items[0].url.split('/').pop()}
{/* 复制链接按钮 - 进一步缩小 */}
))}
) : (
{searchQuery ? '未找到匹配的资源' : '暂无资源'}
{!searchQuery && (
)}
)}
{/* Dialog for adding resources */}
{/* 批量删除确认对话框 */}
>
)
}