live-photo/src/components/FileCard.tsx
2025-08-01 15:41:44 +08:00

168 lines
5.4 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import Image from 'next/image';
import { MoreHorizontal, Eye, EyeOff, Trash2, Download, Play } from 'lucide-react';
import { File } from '@/types/file';
import { formatFileSize, formatDate, isImageFile, isVideoFile } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { useFiles } from '@/hooks/useFiles';
interface FileCardProps {
file: File;
isAdmin?: boolean;
}
export const FileCard: React.FC<FileCardProps> = ({ file, isAdmin = false }) => {
const [previewOpen, setPreviewOpen] = useState(false);
const { deleteFile, toggleFileVisibility } = useFiles();
const handleDelete = async () => {
if (confirm('确定要删除这个文件吗?')) {
await deleteFile(file.id);
}
};
const handleToggleVisibility = async () => {
await toggleFileVisibility(file.id, !file.isVisible);
};
const handleDownload = () => {
const link = document.createElement('a');
link.href = file.filePath;
link.download = file.originalName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const isImage = isImageFile(file.mimeType);
const isVideo = isVideoFile(file.mimeType);
return (
<div className={`file-card ${!file.isVisible ? 'opacity-50' : ''}`}>
<div className="aspect-square relative overflow-hidden bg-muted">
{isImage ? (
<Image
src={file.filePath}
alt={file.originalName}
fill
className="object-cover"
onClick={() => setPreviewOpen(true)}
/>
) : isVideo ? (
<div className="relative w-full h-full flex items-center justify-center">
<video
src={file.filePath}
className="w-full h-full object-cover"
muted
onClick={() => setPreviewOpen(true)}
/>
<div className="absolute inset-0 bg-black/20 flex items-center justify-center">
<Play className="w-12 h-12 text-white/80" />
</div>
</div>
) : (
<div className="w-full h-full flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 mx-auto mb-2 bg-muted rounded flex items-center justify-center">
<span className="text-2xl">📄</span>
</div>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
)}
{!file.isVisible && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<EyeOff className="w-8 h-8 text-white" />
</div>
)}
</div>
<div className="p-4">
<div className="flex items-start justify-between mb-2">
<h3 className="font-medium text-sm line-clamp-2 flex-1 mr-2">
{file.originalName}
</h3>
{isAdmin && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleToggleVisibility}>
{file.isVisible ? (
<>
<EyeOff className="mr-2 h-4 w-4" />
</>
) : (
<>
<Eye className="mr-2 h-4 w-4" />
</>
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDownload}>
<Download className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDelete} className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
<div className="space-y-1 text-xs text-muted-foreground">
<p>{formatFileSize(file.fileSize)}</p>
<p>{formatDate(file.uploadedAt)}</p>
</div>
</div>
{/* Preview Dialog */}
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>{file.originalName}</DialogTitle>
</DialogHeader>
<div className="flex justify-center">
{isImage ? (
<Image
src={file.filePath}
alt={file.originalName}
width={800}
height={600}
className="max-w-full max-h-[600px] object-contain"
/>
) : isVideo ? (
<video
src={file.filePath}
controls
className="max-w-full max-h-[600px]"
/>
) : null}
</div>
</DialogContent>
</Dialog>
</div>
);
};