import React, { useState, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@d8d/shared-ui-components/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog'; import { Card, CardContent } from '@d8d/shared-ui-components/components/ui/card'; import { toast } from 'sonner'; import { fileClientManager, fileClient } from '../api/fileClient'; import MinioUploader from './MinioUploader'; import { Check, Upload, Eye, X, File as FileIcon, Image as ImageIcon } from 'lucide-react'; import { cn } from '../utils/cn'; 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; /** * 仅上传模式 - 只显示上传区域,不显示现有文件列表 * @default false * * 当设置为 true 时: * - 对话框只显示上传区域(MinioUploader) * - 不显示现有文件列表 * - 不调用文件列表查询 API(性能优化) * - 上传成功后自动选中该文件并关闭对话框,直接返回 fileId * * 适用场景:需要快速上传文件而不需要从现有文件中选择时, * 如残疾人上传资料时避免加载大量缩略图导致的性能问题 */ uploadOnly?: boolean; } export const FileSelector: React.FC = ({ value, onChange, accept = '*/*', maxSize = 10, uploadPath = '/files', previewSize = 'medium', showPreview = true, placeholder = '选择文件', title = '选择文件', description = '上传新文件或从已有文件中选择', filterType = 'all', allowMultiple = false, uploadOnly = 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 fileClientManager.get()[':id']['$get']({ param: { id: Number(fileId) } }); if (response.status === 200) { return response.json(); } return null; } catch { return null; } }); const files = await Promise.all(filePromises); return files.filter(file => file !== null); } // 处理单选模式下的单值 if (!Array.isArray(value)) { const response = await fileClientManager.get()[':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]); // 获取文件列表 - uploadOnly 模式下禁用查询以提高性能 const { data: filesData, isLoading, refetch } = useQuery({ queryKey: ['files-for-selection', filterType] as const, queryFn: async () => { const response = await fileClientManager.get().index.$get({ query: { page: 1, pageSize: 50, ...(filterType !== 'all' && { keyword: filterType }) } }); if (response.status !== 200) throw new Error('获取文件列表失败'); return response.json(); }, enabled: isOpen && !uploadOnly, // uploadOnly 模式下不执行查询 }); 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 = async (_fileKey: string, _fileUrl: string, file: File) => { if (uploadOnly) { // uploadOnly 模式:上传成功后自动获取 fileId 并返回 try { // 记录上传开始时间 const uploadStartTime = Date.now(); // 获取刚上传的文件的 ID(通过文件名、大小和时间戳匹配) const response = await fileClientManager.get().index.$get({ query: { page: 1, pageSize: 50, // 获取更多结果以提高匹配概率 } }); if (response.status === 200) { const data = await response.json(); // 多重匹配策略,提高准确性: // 1. 首先通过文件名和大小匹配 // 2. 验证上传时间在最近 30 秒内 // 3. 如果有多个匹配,选择最新的 const matchCandidates = data.data?.filter((f: FileType) => { // 基础匹配:文件名和大小 if (f.name !== file.name || f.size !== file.size) { return false; } // 时间验证:上传时间在最近 30 秒内(避免匹配旧文件) const fileUploadTime = new Date(f.uploadTime || '').getTime(); const timeDiff = uploadStartTime - fileUploadTime; return timeDiff >= 0 && timeDiff < 30000; // 30 秒内 }) || []; if (matchCandidates.length > 0) { // 选择最新的文件(按 uploadTime 降序排序) const uploadedFile = matchCandidates.sort((a: FileType, b: FileType) => { const timeA = new Date(a.uploadTime || '').getTime(); const timeB = new Date(b.uploadTime || '').getTime(); return timeB - timeA; // 降序 })[0]; // 直接返回 fileId 并关闭对话框 if (allowMultiple) { onChange?.([uploadedFile.id]); } else { onChange?.(uploadedFile.id); } setIsOpen(false); toast.success('文件上传成功!'); return; } } // 如果找不到文件,提示用户 toast.error('无法获取上传的文件信息'); } catch (error) { console.error('获取上传文件信息失败:', error); toast.error('获取上传文件信息失败'); } } else { // 默认模式:提示用户从列表中选择 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} {uploadOnly ? '请上传文件' : description}
{/* uploadOnly 模式:只显示上传区域 */} {uploadOnly ? (
) : ( /* 默认模式:显示文件列表 */
{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 && (

暂无文件

请上传文件

)}
)}
)}
{/* uploadOnly 模式下不显示确认/取消按钮 */} {!uploadOnly && ( )}
); }; export default FileSelector;