import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { fileClient } from '@/client/api'; import type { InferResponseType } from 'hono/client'; import { Eye, FileText, Image as ImageIcon } from 'lucide-react'; import { Skeleton } from '@/client/components/ui/skeleton'; import { Card, CardContent } from '@/client/components/ui/card'; import { Badge } from '@/client/components/ui/badge'; import { Button } from '@/client/components/ui/button'; import { toast } from 'sonner'; // 定义文件类型 type FileItem = InferResponseType; interface FilePreviewItemProps { file: FileItem; size: 'small' | 'medium' | 'large'; index?: number; total?: number; onClick?: (file: FileItem) => void; } const FilePreviewItem: React.FC = ({ file, size, index, total, onClick }) => { const getSize = () => { switch (size) { case 'small': return { width: 45, height: 45 }; case 'medium': return { width: 80, height: 80 }; case 'large': return { width: 120, height: 120 }; default: return { width: 80, height: 80 }; } }; const { width, height } = getSize(); const isImage = file.type?.startsWith('image/'); const isVideo = file.type?.startsWith('video/'); const handlePreview = () => { if (onClick) { onClick(file); } else if (isImage || isVideo) { window.open(file.fullUrl, '_blank'); } else { toast.warning('该文件类型不支持预览'); } }; // 获取文件图标 const getFileIcon = (type?: string) => { if (!type) return ; if (type.startsWith('image/')) { return ; } else if (type.startsWith('video/')) { return ; } else if (type.startsWith('audio/')) { return ; } else if (type.includes('pdf')) { return ; } else if (type.includes('word')) { return ; } else if (type.includes('excel') || type.includes('sheet')) { return ; } else { return ; } }; // 获取文件类型标签 const getFileTypeBadge = (type: string) => { if (type.startsWith('image/')) { return { text: '图片', color: 'bg-blue-100 text-blue-800' }; } else if (type.startsWith('video/')) { return { text: '视频', color: 'bg-red-100 text-red-800' }; } else if (type.startsWith('audio/')) { return { text: '音频', color: 'bg-purple-100 text-purple-800' }; } else if (type.includes('pdf')) { return { text: 'PDF', color: 'bg-red-100 text-red-800' }; } else if (type.includes('word')) { return { text: '文档', color: 'bg-blue-100 text-blue-800' }; } else if (type.includes('excel') || type.includes('sheet')) { return { text: '表格', color: 'bg-green-100 text-green-800' }; } else { return { text: '文件', color: 'bg-gray-100 text-gray-800' }; } }; return (
{/* 文件预览容器 */}
{isImage ? ( // 图片预览 {file.name} ) : ( // 非图片文件预览
{getFileIcon(file.type)} {file.name.length > 8 ? `${file.name.substring(0, 6)}...` : file.name}
)} {/* 悬停遮罩 */}
{isImage || isVideo ? '预览' : '查看'}
{/* 序号标记 */} {index !== undefined && total !== undefined && total > 1 && (
{index + 1}
)}
{/* 文件类型标签 */} {getFileTypeBadge(file.type).text}
); }; interface FilePreviewProps { fileIds?: number[]; files?: any[]; maxCount?: number; size?: 'small' | 'medium' | 'large'; showCount?: boolean; onFileClick?: (file: FileItem) => void; className?: string; } const FilePreview: React.FC = ({ fileIds = [], files = [], maxCount = 6, size = 'medium', showCount = true, onFileClick, className = '', }) => { // 合并文件ID和文件对象 const allFileIds = [...fileIds, ...(files?.map(f => f.id) || [])]; const uniqueFileIds = [...new Set(allFileIds)].filter(Boolean); // 使用 React Query 查询文件详情 const { data: fileDetails, isLoading, error } = useQuery({ queryKey: ['files', uniqueFileIds], queryFn: async () => { if (uniqueFileIds.length === 0) return []; const promises = uniqueFileIds.map(async (id) => { try { const response = await fileClient[':id']['$get']({ param: { id: id.toString() } }); if (response.ok) { return response.json(); } return null; } catch (error) { console.error(`获取文件 ${id} 详情失败:`, error); return null; } }); const results = await Promise.all(promises); return results.filter(Boolean) as FileItem[]; }, enabled: uniqueFileIds.length > 0, staleTime: 5 * 60 * 1000, // 5分钟 gcTime: 10 * 60 * 1000, // 10分钟 }); // 加载状态 if (isLoading) { return (
{[...Array(Math.min(maxCount, 3))].map((_, i) => ( ))}

加载中...

); } // 错误状态 if (error) { return (

加载图片失败

); } const displayFiles = fileDetails?.slice(0, maxCount) || []; const remainingCount = Math.max(0, (fileDetails?.length || 0) - maxCount); // 空状态 if (displayFiles.length === 0) { return (

暂无图片

); } return (
{displayFiles.map((file, index) => ( ))}
{/* 剩余数量提示 */} {showCount && remainingCount > 0 && (
还有 {remainingCount} 个文件未显示
)}
); }; // 导出组件和类型 export default FilePreview; export type { FilePreviewProps };