| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- import React, { useState } from 'react';
- import { Modal, Button, Image, message } from 'antd';
- import { useQuery, useQueryClient } from '@tanstack/react-query';
- import { fileClient } from '@/client/api';
- import type { FileType } from '@/server/modules/files/file.schema';
- import MinioUploader from '@/client/admin-shadcn/components/MinioUploader';
- // 定义重载的 props 类型
- interface SingleFileSelectorProps {
- visible: boolean;
- onCancel: () => void;
- onSelect: (file: FileType) => void;
- accept?: string;
- maxSize?: number;
- uploadPath?: string;
- uploadButtonText?: string;
- multiple?: false;
- }
- interface MultipleFileSelectorProps {
- visible: boolean;
- onCancel: () => void;
- onSelect: (files: FileType[]) => void;
- accept?: string;
- maxSize?: number;
- uploadPath?: string;
- uploadButtonText?: string;
- multiple: true;
- }
- type FileSelectorProps = SingleFileSelectorProps | MultipleFileSelectorProps;
- const FileSelector: React.FC<FileSelectorProps> = ({
- visible,
- onCancel,
- onSelect,
- accept = 'image/*',
- maxSize = 5,
- uploadPath = '/uploads',
- uploadButtonText = '上传新文件',
- multiple = false,
- ...props
- }) => {
- const queryClient = useQueryClient();
- const [selectedFiles, setSelectedFiles] = useState<FileType[]>([]);
-
- // 获取文件列表
- const { data: filesData, isLoading } = useQuery({
- queryKey: ['files-for-selection', accept] as const,
- queryFn: async () => {
- const response = await fileClient.$get({
- query: {
- page: 1,
- pageSize: 50,
- keyword: accept.startsWith('image/') ? 'image' : undefined
- }
- });
- if (response.status !== 200) throw new Error('获取文件列表失败');
- return response.json();
- },
- enabled: visible
- });
- // 重置选择状态
- React.useEffect(() => {
- if (!visible) {
- setSelectedFiles([]);
- }
- }, [visible]);
- const handleSelectFile = (file: FileType) => {
- if (!file) {
- console.error('No file provided to handleSelectFile');
- return;
- }
- if (multiple) {
- // 多选模式
- const newSelection = selectedFiles.some(f => f.id === file.id)
- ? selectedFiles.filter(f => f.id !== file.id)
- : [...selectedFiles, file];
-
- setSelectedFiles(newSelection);
- } else {
- // 单选模式
- setSelectedFiles([file]);
- }
- };
- const handleConfirm = () => {
- if (selectedFiles.length === 0) {
- message.warning('请先选择文件');
- return;
- }
- if (multiple) {
- // 多选模式
- (onSelect as MultipleFileSelectorProps['onSelect'])(selectedFiles);
- } else {
- // 单选模式
- (onSelect as SingleFileSelectorProps['onSelect'])(selectedFiles[0]);
- }
-
- onCancel();
- };
- const handleUploadSuccess = (fileKey: string, fileUrl: string, file: any) => {
- message.success('文件上传成功!请从列表中选择新上传的文件');
- // 刷新文件列表
- queryClient.invalidateQueries({ queryKey: ['files-for-selection', accept] });
- };
- const filteredFiles = Array.isArray(filesData?.data)
- ? filesData.data.filter((f: any) => !accept || f?.type?.startsWith(accept.replace('*', '')))
- : [];
- const isFileSelected = (file: FileType) => {
- return selectedFiles.some(f => f.id === file.id);
- };
- const getSelectionText = () => {
- if (multiple) {
- return selectedFiles.length > 0 ? `已选择 ${selectedFiles.length} 个文件` : '请选择文件';
- }
- return selectedFiles.length > 0 ? `已选择: ${selectedFiles[0].name}` : '请选择文件';
- };
- return (
- <Modal
- title="选择文件"
- open={visible}
- onCancel={onCancel}
- width={800}
- onOk={handleConfirm}
- okText="确认选择"
- cancelText="取消"
- okButtonProps={{ disabled: selectedFiles.length === 0 }}
- >
- <div style={{ marginBottom: 16 }}>
- <MinioUploader
- uploadPath={uploadPath}
- accept={accept}
- maxSize={maxSize}
- onUploadSuccess={handleUploadSuccess}
- buttonText={uploadButtonText}
- />
- <span style={{ color: '#666', fontSize: '12px', marginLeft: 8 }}>
- 上传后请从下方列表中选择文件
- </span>
- </div>
- <div style={{ marginBottom: 16 }}>
- <div style={{
- padding: '8px 12px',
- backgroundColor: '#f6ffed',
- border: '1px solid #b7eb8f',
- borderRadius: '4px',
- color: '#52c41a'
- }}>
- {getSelectionText()}
- </div>
- </div>
- <div style={{ maxHeight: 400, overflowY: 'auto' }}>
- {isLoading ? (
- <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
- 加载中...
- </div>
- ) : filteredFiles.length > 0 ? (
- filteredFiles.map((file) => (
- <div
- key={file.id}
- style={{
- display: 'flex',
- alignItems: 'center',
- padding: '12px',
- border: `1px solid ${isFileSelected(file) ? '#1890ff' : '#f0f0f0'}`,
- borderRadius: '4px',
- marginBottom: '8px',
- cursor: 'pointer',
- transition: 'all 0.3s',
- backgroundColor: isFileSelected(file) ? '#e6f7ff' : ''
- }}
- onClick={() => handleSelectFile(file)}
- onMouseEnter={(e) => {
- if (!isFileSelected(file)) {
- e.currentTarget.style.backgroundColor = '#f6ffed';
- e.currentTarget.style.borderColor = '#b7eb8f';
- }
- }}
- onMouseLeave={(e) => {
- if (!isFileSelected(file)) {
- e.currentTarget.style.backgroundColor = '';
- e.currentTarget.style.borderColor = '#f0f0f0';
- }
- }}
- >
- <Image
- src={file.fullUrl}
- alt={file.name}
- style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4, marginRight: 12 }}
- preview={false}
- />
- <div style={{ flex: 1 }}>
- <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>{file.name}</div>
- <div style={{ color: '#666', fontSize: '12px' }}>
- ID: {file.id} | 大小: {((file.size || 0) / 1024 / 1024).toFixed(2)}MB
- </div>
- </div>
- <Button
- type={isFileSelected(file) ? 'primary' : 'default'}
- size="small"
- >
- {isFileSelected(file) ? '已选择' : '选择'}
- </Button>
- </div>
- ))
- ) : (
- <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
- 暂无符合条件的文件,请先上传
- </div>
- )}
- </div>
- </Modal>
- );
- };
- // 类型守卫函数
- function isMultipleSelector(props: FileSelectorProps): props is MultipleFileSelectorProps {
- return props.multiple === true;
- }
- function isSingleSelector(props: FileSelectorProps): props is SingleFileSelectorProps {
- return props.multiple === false || props.multiple === undefined;
- }
- export default FileSelector;
|