|
|
@@ -1,11 +1,12 @@
|
|
|
-import React from 'react';
|
|
|
+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 { File as FileType } from '@/server/modules/files/file.entity';
|
|
|
import MinioUploader from '@/client/admin/components/MinioUploader';
|
|
|
|
|
|
-interface FileSelectorProps {
|
|
|
+// 定义重载的 props 类型
|
|
|
+interface SingleFileSelectorProps {
|
|
|
visible: boolean;
|
|
|
onCancel: () => void;
|
|
|
onSelect: (file: FileType) => void;
|
|
|
@@ -13,8 +14,22 @@ interface FileSelectorProps {
|
|
|
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,
|
|
|
@@ -22,9 +37,12 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
accept = 'image/*',
|
|
|
maxSize = 5,
|
|
|
uploadPath = '/uploads',
|
|
|
- uploadButtonText = '上传新文件'
|
|
|
+ uploadButtonText = '上传新文件',
|
|
|
+ multiple = false,
|
|
|
+ ...props
|
|
|
}) => {
|
|
|
const queryClient = useQueryClient();
|
|
|
+ const [selectedFiles, setSelectedFiles] = useState<FileType[]>([]);
|
|
|
|
|
|
// 获取文件列表
|
|
|
const { data: filesData, isLoading } = useQuery({
|
|
|
@@ -43,9 +61,47 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
enabled: visible
|
|
|
});
|
|
|
|
|
|
- const handleSelectFile = (file: any) => {
|
|
|
- onSelect(file);
|
|
|
- message.success(`已选择文件: ${file.name}`);
|
|
|
+ // 重置选择状态
|
|
|
+ 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) => {
|
|
|
@@ -54,9 +110,20 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
queryClient.invalidateQueries({ queryKey: ['files-for-selection', accept] });
|
|
|
};
|
|
|
|
|
|
- const filteredFiles = filesData?.data?.filter((f: any) =>
|
|
|
- !accept || f.type?.startsWith(accept.replace('*', ''))
|
|
|
- ) || [];
|
|
|
+ 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
|
|
|
@@ -64,7 +131,10 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
open={visible}
|
|
|
onCancel={onCancel}
|
|
|
width={800}
|
|
|
- footer={null}
|
|
|
+ onOk={handleConfirm}
|
|
|
+ okText="确认选择"
|
|
|
+ cancelText="取消"
|
|
|
+ okButtonProps={{ disabled: selectedFiles.length === 0 }}
|
|
|
>
|
|
|
<div style={{ marginBottom: 16 }}>
|
|
|
<MinioUploader
|
|
|
@@ -79,33 +149,50 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
</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: any) => (
|
|
|
+ filteredFiles.map((file: FileType) => (
|
|
|
<div
|
|
|
key={file.id}
|
|
|
style={{
|
|
|
display: 'flex',
|
|
|
alignItems: 'center',
|
|
|
padding: '12px',
|
|
|
- border: '1px solid #f0f0f0',
|
|
|
+ border: `1px solid ${isFileSelected(file) ? '#1890ff' : '#f0f0f0'}`,
|
|
|
borderRadius: '4px',
|
|
|
marginBottom: '8px',
|
|
|
cursor: 'pointer',
|
|
|
- transition: 'all 0.3s'
|
|
|
+ transition: 'all 0.3s',
|
|
|
+ backgroundColor: isFileSelected(file) ? '#e6f7ff' : ''
|
|
|
}}
|
|
|
onClick={() => handleSelectFile(file)}
|
|
|
onMouseEnter={(e) => {
|
|
|
- e.currentTarget.style.backgroundColor = '#f6ffed';
|
|
|
- e.currentTarget.style.borderColor = '#b7eb8f';
|
|
|
+ if (!isFileSelected(file)) {
|
|
|
+ e.currentTarget.style.backgroundColor = '#f6ffed';
|
|
|
+ e.currentTarget.style.borderColor = '#b7eb8f';
|
|
|
+ }
|
|
|
}}
|
|
|
onMouseLeave={(e) => {
|
|
|
- e.currentTarget.style.backgroundColor = '';
|
|
|
- e.currentTarget.style.borderColor = '#f0f0f0';
|
|
|
+ if (!isFileSelected(file)) {
|
|
|
+ e.currentTarget.style.backgroundColor = '';
|
|
|
+ e.currentTarget.style.borderColor = '#f0f0f0';
|
|
|
+ }
|
|
|
}}
|
|
|
>
|
|
|
<Image
|
|
|
@@ -120,7 +207,12 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
ID: {file.id} | 大小: {((file.size || 0) / 1024 / 1024).toFixed(2)}MB
|
|
|
</div>
|
|
|
</div>
|
|
|
- <Button type="primary" size="small">选择</Button>
|
|
|
+ <Button
|
|
|
+ type={isFileSelected(file) ? 'primary' : 'default'}
|
|
|
+ size="small"
|
|
|
+ >
|
|
|
+ {isFileSelected(file) ? '已选择' : '选择'}
|
|
|
+ </Button>
|
|
|
</div>
|
|
|
))
|
|
|
) : (
|
|
|
@@ -133,4 +225,13 @@ const FileSelector: React.FC<FileSelectorProps> = ({
|
|
|
);
|
|
|
};
|
|
|
|
|
|
+// 类型守卫函数
|
|
|
+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;
|