|
|
@@ -0,0 +1,237 @@
|
|
|
+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;
|