import React, { useState, useEffect, useRef } 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, Image as ImageIcon } from 'lucide-react'; import { cn } from '@/client/lib/utils'; import type { InferResponseType } from 'hono/client'; type FileType = InferResponseType['data'][0] interface ImageSelectorProps { value?: number | null | number[]; onChange?: (fileId: number | null | number[]) => void; accept?: string; maxSize?: number; uploadPath?: string; uploadButtonText?: string; previewSize?: 'small' | 'medium' | 'large'; showPreview?: boolean; placeholder?: string; title?: string; description?: string; filterType?: 'image' | 'all' | string; allowMultiple?: boolean; selectedFiles?: number[]; onMultipleSelect?: (fileIds: number[]) => void; } export const ImageSelector: React.FC = ({ value, onChange, accept = 'image/*', maxSize = 5, uploadPath = '/images', uploadButtonText = '上传图片', previewSize = 'medium', showPreview = true, placeholder = '选择图片', title = '选择图片', description = '上传新图片或从已有图片中选择', filterType = 'image', allowMultiple = false, selectedFiles = [], onMultipleSelect, }) => { const [isOpen, setIsOpen] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [localSelectedFiles, setLocalSelectedFiles] = useState(selectedFiles); const prevSelectedFilesRef = useRef(selectedFiles); // 获取当前选中的文件详情 const { data: currentFile } = useQuery({ queryKey: ['file-detail', value], queryFn: async () => { if (!value) return null; const response = await fileClient[':id']['$get']({ param: { id: value.toString() } }); if (response.status !== 200) throw new Error('获取文件详情失败'); return response.json(); }, enabled: !!value, }); // 当对话框打开时,设置当前选中的图片 useEffect(() => { if (isOpen) { if (allowMultiple) { setLocalSelectedFiles(selectedFiles); } else if (value && currentFile) { setSelectedFile(currentFile); } } }, [isOpen, value, currentFile, allowMultiple]); // 修复无限循环问题:使用ref比较数组内容 useEffect(() => { const areArraysEqual = (a: number[], b: number[]) => { if (a.length !== b.length) return false; const sortedA = [...a].sort(); const sortedB = [...b].sort(); return sortedA.every((val, index) => val === sortedB[index]); }; if (!areArraysEqual(prevSelectedFilesRef.current, selectedFiles)) { setLocalSelectedFiles(selectedFiles); prevSelectedFilesRef.current = selectedFiles; } }, [selectedFiles]); // 获取图片列表 const { data: filesData, isLoading, refetch } = useQuery({ queryKey: ['images-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 images = filesData?.data?.filter((f: any) => { if (filterType === 'all') return true; if (filterType === 'image') return f?.type?.startsWith('image/'); return f?.type?.includes(filterType); }) || []; const handleSelectImage = (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 (onMultipleSelect) { onMultipleSelect(localSelectedFiles); } else 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); setLocalSelectedFiles(selectedFiles); }; 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 handleRemoveImage = (e: React.MouseEvent) => { e.stopPropagation(); onChange?.(null); }; const isSelected = (fileId: number) => { if (allowMultiple) { return localSelectedFiles.includes(fileId); } return selectedFile?.id === fileId; }; return ( <>
{showPreview && (
setIsOpen(true)} > {currentFile ? ( {currentFile.name} ) : (
{placeholder}
)}
{currentFile && ( )}
{currentFile && (

当前: {currentFile.name}

)}
)} {!showPreview && ( )}
{title} {description}
{/* 图片列表 */}
{isLoading ? (

加载中...

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

上传新图片

{/* 现有图片列表 */} {images.map((file) => (
handleSelectImage(file)} >
{file.name} {isSelected(file.id) && (
)}
{ e.stopPropagation(); window.open(file.fullUrl, '_blank'); }} />

{file.name}

))} {/* 空状态 - 当没有图片时显示 */} {images.length === 0 && (

暂无图片

请上传图片文件

)}
)}
); }; export default ImageSelector;