This commit is contained in:
eric
2025-11-13 05:05:53 +08:00
parent cbdc65f56e
commit 891af61146
691 changed files with 83952 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
'use client'
import React, { useState } from 'react';
export const runtime = 'edge'
const FaviconDownloader: React.FC = () => {
const [url, setUrl] = useState('');
const [faviconUrl, setFaviconUrl] = useState('');
const [showError, setShowError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleFetch = async (method: 'direct' | 'html') => {
setIsLoading(true);
setShowError('');
const apiUrl = method === 'direct'
? `/api/favicon?domain=${url}`
: `/api/favicon-im?domain=${url}`;
try {
console.log(`Fetching favicon ${method === 'direct' ? 'directly' : 'from HTML'}: ${apiUrl}`);
const response = await fetch(apiUrl);
console.log(`Response status: ${response.status}`);
if (!response.ok) {
throw new Error('无法访问该网站或未找到 Favicon');
}
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
setFaviconUrl(blobUrl);
} catch (error: unknown) {
console.error(`Error fetching favicon ${method}:`, error);
setShowError(error instanceof Error ? error.message : '发生错误,请重试。');
} finally {
setIsLoading(false);
}
};
const downloadFavicon = () => {
if (!faviconUrl) {
setShowError('请先获取 Favicon。');
return;
}
const link = document.createElement('a');
link.href = faviconUrl;
link.download = 'favicon.ico';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100">
<div className="max-w-2xl mx-auto py-12 px-4">
<div className="bg-white rounded-lg shadow-lg p-8">
<h1 className="text-3xl font-bold text-center text-gray-800 mb-8">
Favicon
</h1>
<div className="space-y-6">
<div className="relative">
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="请输入网站地址 (例如: https://example.com)"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
/>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button
onClick={() => handleFetch('direct')}
disabled={isLoading || !url}
className="flex-1 bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? '获取中...' : '通过 Google Favicon 获取'}
</button>
<button
onClick={() => handleFetch('html')}
disabled={isLoading || !url}
className="flex-1 bg-green-500 text-white px-6 py-3 rounded-lg hover:bg-green-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? '获取中...' : '通过favicon im获取'}
</button>
</div>
{faviconUrl && (
<div className="text-center p-6 bg-gray-50 rounded-lg">
<img
src={faviconUrl}
alt="Favicon"
className="w-16 h-16 mx-auto mb-4 shadow-md rounded"
/>
<p className="text-gray-600 mb-4">Favicon </p>
<button
onClick={downloadFavicon}
className="bg-indigo-500 text-white px-6 py-2 rounded-lg hover:bg-indigo-600 transition"
>
Favicon
</button>
</div>
)}
{showError && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-center">{showError}</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default FaviconDownloader;

View File

@@ -0,0 +1,947 @@
'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 = () => (
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-2">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((index) => (
<div key={index} className="bg-white rounded-lg border shadow-sm animate-pulse">
<div className="aspect-square bg-gray-200 rounded-t-lg" />
<div className="p-2 space-y-1">
<div className="h-2 w-3/4 bg-gray-200 rounded" />
<div className="h-2 w-1/2 bg-gray-200 rounded" />
</div>
</div>
))}
</div>
);
// 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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
resource: {
image: undefined,
},
},
})
const [resources, setResources] = useState<LocalResourceMetadata[]>([]);
const [error, setError] = useState<string | null>(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<string | null>(null); // State to hold the selected image for preview
const [uploadSpeed, setUploadSpeed] = useState<number>(0);
const [abortController, setAbortController] = useState<AbortController | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [selectedFile, setSelectedFile] = useState<File | null>(null); // 新增文件状态
const [searchQuery, setSearchQuery] = useState("");
const [selectedResources, setSelectedResources] = useState<Set<string>>(new Set());
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [resourceReferences, setResourceReferences] = useState<Record<string, Array<{ type: string; location: string; title?: string }>>>({});
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<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const file = e.dataTransfer.files[0];
if (file) {
handleImageChange(file);
}
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
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<string> => {
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 && (
<div className="fixed top-0 left-0 right-0 z-[100]">
<div className="bg-white/80 backdrop-blur-sm shadow-lg border-b px-4 py-3">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Icons.upload className="h-4 w-4 animate-pulse text-blue-600" />
<span className="text-sm font-medium text-gray-700">...</span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-sm text-gray-600">
<span className="font-medium">{uploadProgress}%</span>
<span className="text-gray-400"></span>
<span>{(uploadSpeed / (1024 * 1024)).toFixed(2)} MB/s</span>
</div>
<Button
variant="ghost"
onClick={cancelUpload}
size="sm"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Icons.x className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
<div className="relative w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
{/* 背景动画效果 */}
<div
className="absolute inset-0 bg-gradient-to-r from-blue-500 to-blue-600 animate-pulse opacity-20"
/>
{/* 主进度条 */}
<div
className="relative h-full bg-gradient-to-r from-blue-500 to-blue-600 rounded-full transition-all duration-300 ease-out"
style={{
width: `${uploadProgress}%`,
boxShadow: '0 0 10px rgba(37, 99, 235, 0.5)'
}}
>
{/* 光晕效果 */}
<div
className="absolute right-0 top-0 h-full w-4 bg-white opacity-30 transform -skew-x-12 animate-[shimmer_1s_infinite]"
/>
</div>
</div>
</div>
</div>
</div>
)}
<div className="space-y-4 p-6">
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="relative">
<Icons.search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索资源..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 max-w-md"
/>
</div>
{filteredResources.length > 0 && (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={selectAllResources}
className="flex items-center gap-2"
>
{selectedResources.size === filteredResources.length ? (
<Icons.checkSquare className="h-4 w-4" />
) : (
<Icons.square className="h-4 w-4" />
)}
({selectedResources.size}/{filteredResources.length})
</Button>
{selectedResources.size > 0 && (
<Button
variant="destructive"
size="sm"
onClick={handleBatchDelete}
className="flex items-center gap-2"
>
<Icons.trash2 className="h-4 w-4" />
({selectedResources.size})
</Button>
)}
</div>
)}
</div>
<Button onClick={() => setIsDialogOpen(true)}>
<Icons.plus className="mr-2 h-4 w-4" />
</Button>
</div>
{/* 添加搜索框 */}
</div>
{error && (
<div className="bg-red-50 border-l-4 border-red-400 p-4">
<div className="flex items-center">
<Icons.alertCircle className="h-5 w-5 text-red-400 mr-2" />
<p className="text-red-700">{error}</p>
</div>
</div>
)}
{isLoading ? (
<ResourceGridSkeleton />
) : filteredResources.length > 0 ? (
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-2">
{filteredResources.map((resource, index) => (
<div
key={index}
className={`group bg-white rounded-lg border shadow-sm hover:shadow-md transition-all duration-200 ${selectedResources.has(resource.id) ? 'ring-2 ring-blue-500 border-blue-500' : ''
}`}
>
<div className="relative aspect-square">
{/* 选择框 */}
<div className="absolute top-2 left-2 z-10">
<Button
variant="ghost"
size="sm"
onClick={() => toggleResourceSelection(resource.id)}
className="h-6 w-6 p-0 bg-white/80 backdrop-blur-sm hover:bg-white/90"
>
{selectedResources.has(resource.id) ? (
<Icons.checkSquare className="h-3 w-3 text-blue-600" />
) : (
<Icons.square className="h-3 w-3 text-gray-600" />
)}
</Button>
</div>
<img
src={resource.items[0].url}
alt={`Resource ${index + 1}`}
className="w-full h-full object-cover rounded-t-lg"
loading="lazy"
/>
{/* 图片遮罩层和预览按钮 - 进一步缩小 */}
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center">
<a
href={resource.items[0].url}
target="_blank"
rel="noopener noreferrer"
className="bg-white/10 backdrop-blur-sm text-white px-1.5 py-0.5 rounded-full
hover:bg-white/20 transition-colors duration-200 flex items-center gap-0.5 text-[10px]"
>
<Icons.upload className="h-2 w-2" />
</a>
</div>
</div>
<div className="p-1.5 space-y-1">
{/* 文件名和上传时间 */}
<div className="space-y-0.5">
<p className="text-[10px] text-gray-500 truncate" title={resource.items[0].url}>
{resource.items[0].url.split('/').pop()}
</p>
</div>
{/* 复制链接按钮 - 进一步缩小 */}
<Button
variant="outline"
size="sm"
onClick={() => copyToClipboard(resource.items[0].url)}
className="w-full h-5 text-[8px] flex items-center justify-center gap-0.5 hover:bg-gray-50"
>
<Icons.copy className="h-2 w-2" />
</Button>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center h-64 border-2 border-dashed rounded-lg bg-muted/50">
<Icons.inbox className="h-10 w-10 text-gray-400 mb-2" />
<p className="text-gray-500">
{searchQuery ? '未找到匹配的资源' : '暂无资源'}
</p>
{!searchQuery && (
<Button
variant="outline"
onClick={() => setIsDialogOpen(true)}
className="mt-4"
>
</Button>
)}
</div>
)}
</div>
{/* Dialog for adding resources */}
<Dialog open={isDialogOpen} onOpenChange={(open) => {
setIsDialogOpen(open);
if (!open) {
// 关闭对话框时重置状态
setSelectedImage(null);
setSelectedFile(null);
form.reset();
}
}}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="resource.image"
render={() => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Card
className={`border-2 border-dashed ${selectedImage ? 'border-blue-400' : 'border-gray-200'
} hover:border-blue-400 transition-colors duration-200`}
>
<CardContent
className="flex flex-col items-center justify-center p-6 cursor-pointer"
onDrop={handleDrop}
onDragOver={handleDragOver}
onClick={() => {
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 ? (
<div className="relative w-full">
<img
src={selectedImage}
alt="Preview"
className="w-full h-48 object-cover rounded-lg"
/>
<Button
type="button"
variant="destructive"
size="icon"
className="absolute top-2 right-2"
onClick={(e) => {
e.stopPropagation();
setSelectedImage(null);
}}
>
<Icons.x className="h-4 w-4" />
</Button>
</div>
) : (
<>
<Icons.upload className="h-12 w-12 text-gray-400 mb-4" />
<div className="space-y-2 text-center">
<p className="text-sm text-gray-600">
</p>
<p className="text-xs text-gray-400">
JPGPNG 5MB
</p>
</div>
</>
)}
</CardContent>
</Card>
</FormControl>
<FormDescription>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="submit"
disabled={!selectedFile || isUploading}
className="w-full"
>
{isUploading ? (
<>
<Icons.loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
'开始上传'
)}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
{/* 批量删除确认对话框 */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent className="sm:max-w-[600px] max-h-[80vh] flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2">
<Icons.alertCircle className="h-5 w-5 text-red-500" />
</DialogTitle>
<DialogDescription>
{selectedResources.size}
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-hidden flex flex-col space-y-4">
{/* 显示有引用的资源警告 */}
{Object.entries(resourceReferences).some(([_, refs]) => refs.length > 0) && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 flex-shrink-0">
<div className="flex items-start gap-2">
<Icons.alertCircle className="h-5 w-5 text-yellow-600 mt-0.5 flex-shrink-0" />
<div className="space-y-2 min-w-0">
<p className="text-sm font-medium text-yellow-800">
使
</p>
<div className="max-h-24 overflow-y-auto space-y-2">
{Object.entries(resourceReferences).map(([resourcePath, refs]) =>
refs.length > 0 && (
<div key={resourcePath} className="text-sm text-yellow-700">
<p className="font-medium truncate" title={resourcePath}>
{resourcePath.split('/').pop()}
</p>
<ul className="ml-4 space-y-1">
{refs.slice(0, 3).map((ref, index) => (
<li key={index} className="text-xs truncate">
{ref.location}
</li>
))}
{refs.length > 3 && (
<li className="text-xs text-yellow-600">
{refs.length - 3} ...
</li>
)}
</ul>
</div>
)
)}
</div>
<p className="text-xs text-yellow-600">
</p>
</div>
</div>
</div>
)}
{/* 显示将要删除的资源列表 */}
<div className="flex-1 overflow-hidden">
<div className="flex items-center justify-between mb-3">
<p className="text-sm font-medium text-gray-700">
({selectedResources.size} )
</p>
{selectedResources.size > 12 && (
<p className="text-xs text-gray-500">
12 {selectedResources.size}
</p>
)}
</div>
<div className="overflow-y-auto max-h-48">
{selectedResources.size <= 12 ? (
// 少量资源时显示网格
<div className="grid grid-cols-4 gap-2">
{filteredResources
.filter(r => selectedResources.has(r.id))
.map((resource, index) => (
<div key={index} className="relative group">
<img
src={resource.items[0].url}
alt="Resource"
className="w-full aspect-square object-cover rounded border"
/>
<div className="absolute inset-0 bg-red-500/20 rounded"></div>
<div className="absolute bottom-1 left-1 right-1">
<div className="bg-black/60 text-white text-xs px-1 py-0.5 rounded truncate">
{resource.items[0].url.split('/').pop()}
</div>
</div>
</div>
))}
</div>
) : (
// 大量资源时显示列表
<div className="space-y-2">
{filteredResources
.filter(r => selectedResources.has(r.id))
.slice(0, 12)
.map((resource, index) => (
<div key={index} className="flex items-center gap-3 p-2 bg-gray-50 rounded-lg">
<img
src={resource.items[0].url}
alt="Resource"
className="w-12 h-12 object-cover rounded border flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{resource.items[0].url.split('/').pop()}
</p>
<p className="text-xs text-gray-500 truncate">
{resource.items[0].url}
</p>
</div>
<div className="w-4 h-4 bg-red-500/20 rounded border border-red-300 flex-shrink-0"></div>
</div>
))}
{selectedResources.size > 12 && (
<div className="text-center py-3 border-t border-gray-200">
<p className="text-sm text-gray-500">
{selectedResources.size - 12}
</p>
</div>
)}
</div>
)}
</div>
</div>
</div>
<DialogFooter className="gap-2 flex-shrink-0 border-t pt-4">
<Button
variant="outline"
onClick={() => setIsDeleteDialogOpen(false)}
disabled={isDeleting}
>
</Button>
<Button
variant="destructive"
onClick={confirmDelete}
disabled={isDeleting}
>
{isDeleting ? (
<>
<Icons.loader2 className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<Icons.trash2 className="mr-2 h-4 w-4" />
({selectedResources.size})
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Toaster />
</>
)
}