import React, { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@/client/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog'; import { Card, CardContent } from '@/client/components/ui/card'; import { toast } from 'sonner'; import { fileClient } from '@/client/api'; import MinioUploader from '@/client/admin/components/MinioUploader'; import { Check, Upload, Eye, X, File as FileIcon, Image as ImageIcon } from 'lucide-react'; import { cn } from '@/client/lib/utils'; import type { InferResponseType } from 'hono/client'; type FileType = InferResponseType['data'][0] export interface FileSelectorProps { value?: number | null | number[]; onChange?: (fileId: number | null | number[]) => void; accept?: string; maxSize?: number; uploadPath?: string; previewSize?: 'small' | 'medium' | 'large'; showPreview?: boolean; placeholder?: string; title?: string; description?: string; filterType?: 'image' | 'all' | string; allowMultiple?: boolean; } export const FileSelector: React.FC = ({ value, onChange, accept = '*/*', maxSize = 10, uploadPath = '/files', previewSize = 'medium', showPreview = true, placeholder = '选择文件', title = '选择文件', description = '上传新文件或从已有文件中选择', filterType = 'all', allowMultiple = false, }) => { const [isOpen, setIsOpen] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [localSelectedFiles, setLocalSelectedFiles] = useState([]); // 获取当前选中的文件详情 - 支持单值和数组 const { data: currentFiles } = useQuery({ queryKey: ['file-details', value, allowMultiple], queryFn: async (): Promise => { if (!value) return []; // 处理多选模式下的数组值 if (allowMultiple && Array.isArray(value)) { if (value.length === 0) return []; // 批量获取多个文件详情 const filePromises = value.map(async (fileId) => { try { const response = await fileClient[':id']['$get']({ param: { id: Number(fileId) } }); if (response.status === 200) { return response.json(); } return null; } catch (error) { console.error(`获取文件 ${fileId} 详情失败:`, error); return null; } }); const files = await Promise.all(filePromises); return files.filter(file => file !== null); } // 处理单选模式下的单值 if (!Array.isArray(value)) { const response = await fileClient[':id']['$get']({ param: { id: Number(value) } }); if (response.status !== 200) throw new Error('获取文件详情失败'); return [await response.json()]; } return []; }, enabled: !!value, }); // 当对话框打开时,设置当前选中的文件 useEffect(() => { if (isOpen) { if (allowMultiple) { // 在多选模式下,使用 value 数组初始化本地选择 const initialSelection = Array.isArray(value) ? value : []; setLocalSelectedFiles(initialSelection); } else if (value && currentFiles && currentFiles.length > 0) { setSelectedFile(currentFiles[0]); } } }, [isOpen, value, currentFiles, allowMultiple]); // 获取文件列表 const { data: filesData, isLoading, refetch } = useQuery({ queryKey: ['files-for-selection', filterType] as const, queryFn: async () => { const response = await fileClient.$get({ query: { page: 1, pageSize: 50, ...(filterType !== 'all' && { keyword: filterType }) } }); if (response.status !== 200) throw new Error('获取文件列表失败'); return response.json(); }, enabled: isOpen, }); const files = filesData?.data?.filter((f) => { if (filterType === 'all') return true; if (filterType === 'image') return f?.type?.startsWith('image/'); return f?.type?.includes(filterType); }) || []; const handleSelectFile = (file: FileType) => { if (allowMultiple) { setLocalSelectedFiles(prev => { const newSelection = prev.includes(file.id) ? prev.filter(id => id !== file.id) : [...prev, file.id]; return newSelection; }); } else { setSelectedFile(prevSelected => { if (prevSelected?.id === file.id) { return null; } return file; }); } }; const handleConfirm = () => { if (allowMultiple) { if (onChange) { onChange(localSelectedFiles); } setIsOpen(false); return; } if (!selectedFile) { toast.warning('请选择一个文件'); return; } if (onChange) { onChange(selectedFile.id); } setIsOpen(false); setSelectedFile(null); }; const handleCancel = () => { setIsOpen(false); setSelectedFile(null); // 取消时重置为初始的 value 值 const initialSelection = allowMultiple && Array.isArray(value) ? value : []; setLocalSelectedFiles(initialSelection); }; const handleUploadSuccess = () => { toast.success('文件上传成功!请从列表中选择新上传的文件'); refetch(); }; const getPreviewSize = () => { switch (previewSize) { case 'small': return 'h-16 w-16'; case 'medium': return 'h-24 w-24'; case 'large': return 'h-32 w-32'; default: return 'h-24 w-24'; } }; const getFileIcon = (fileType: string) => { if (fileType.startsWith('image/')) { return ; } if (fileType.startsWith('video/')) { return ; } if (fileType.startsWith('audio/')) { return ; } if (fileType.includes('pdf')) { return ; } if (fileType.includes('text')) { return ; } return ; }; const handleRemoveFile = (e: React.MouseEvent) => { e.stopPropagation(); if (allowMultiple && Array.isArray(value)) { // 在多选模式下,移除所有选中文件 onChange?.([]); } else { // 在单选模式下,设置为null onChange?.(null); } }; const isSelected = (fileId: number) => { if (allowMultiple) { return localSelectedFiles.includes(fileId); } return selectedFile?.id === fileId; }; return ( <>
{showPreview && (
{/* 预览区域 */}
{allowMultiple && Array.isArray(currentFiles) && currentFiles.length > 0 ? ( // 多选模式下的预览 currentFiles.map((file) => (
setIsOpen(true)} > {file?.type ? (
{file.type.startsWith('image/') ? ( {file.name} ) : (
{getFileIcon(file.type)} {file.name}
)}
) : (
{placeholder}
)}
)) ) : !allowMultiple && currentFiles && currentFiles.length > 0 ? ( // 单选模式下的预览
setIsOpen(true)} > {currentFiles[0]?.type ? (
{currentFiles[0].type.startsWith('image/') ? ( {currentFiles[0].name} ) : (
{getFileIcon(currentFiles[0].type)} {currentFiles[0].name}
)}
) : (
{placeholder}
)}
{currentFiles[0] && ( )}
) : ( // 没有选中文件时的占位符
setIsOpen(true)} >
{placeholder}
)}
{!allowMultiple && currentFiles && currentFiles.length > 0 && (

当前: {currentFiles[0].name}

)} {allowMultiple && currentFiles && currentFiles.length > 0 && (

已选择 {currentFiles.length} 个文件

)}
)} {!showPreview && ( )}
{title} {description}
{/* 文件列表 */}
{isLoading ? (

加载中...

) : (
{/* 上传区域 - 作为第一项 */}

上传新文件

{/* 现有文件列表 */} {files.map((file) => (
handleSelectFile(file)} >
{file?.type?.startsWith('image/') ? ( {file.name} ) : (
{file.type && getFileIcon(file.type)}

{file.name}

)} {isSelected(file.id) && (
)}
{ e.stopPropagation(); window.open(file.fullUrl, '_blank'); }} />

{file.name}

))} {/* 空状态 - 当没有文件时显示 */} {files.length === 0 && (

暂无文件

请上传文件

)}
)}
); }; export default FileSelector;