FileSelector.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import React, { useState } from 'react';
  2. import { Modal, Button, Image, message } from 'antd';
  3. import { useQuery, useQueryClient } from '@tanstack/react-query';
  4. import { fileClient } from '@/client/api';
  5. import type { FileType } from '@/server/modules/files/file.schema';
  6. import MinioUploader from '@/client/admin-shadcn/components/MinioUploader';
  7. // 定义重载的 props 类型
  8. interface SingleFileSelectorProps {
  9. visible: boolean;
  10. onCancel: () => void;
  11. onSelect: (file: FileType) => void;
  12. accept?: string;
  13. maxSize?: number;
  14. uploadPath?: string;
  15. uploadButtonText?: string;
  16. multiple?: false;
  17. }
  18. interface MultipleFileSelectorProps {
  19. visible: boolean;
  20. onCancel: () => void;
  21. onSelect: (files: FileType[]) => void;
  22. accept?: string;
  23. maxSize?: number;
  24. uploadPath?: string;
  25. uploadButtonText?: string;
  26. multiple: true;
  27. }
  28. type FileSelectorProps = SingleFileSelectorProps | MultipleFileSelectorProps;
  29. const FileSelector: React.FC<FileSelectorProps> = ({
  30. visible,
  31. onCancel,
  32. onSelect,
  33. accept = 'image/*',
  34. maxSize = 5,
  35. uploadPath = '/uploads',
  36. uploadButtonText = '上传新文件',
  37. multiple = false,
  38. ...props
  39. }) => {
  40. const queryClient = useQueryClient();
  41. const [selectedFiles, setSelectedFiles] = useState<FileType[]>([]);
  42. // 获取文件列表
  43. const { data: filesData, isLoading } = useQuery({
  44. queryKey: ['files-for-selection', accept] as const,
  45. queryFn: async () => {
  46. const response = await fileClient.$get({
  47. query: {
  48. page: 1,
  49. pageSize: 50,
  50. keyword: accept.startsWith('image/') ? 'image' : undefined
  51. }
  52. });
  53. if (response.status !== 200) throw new Error('获取文件列表失败');
  54. return response.json();
  55. },
  56. enabled: visible
  57. });
  58. // 重置选择状态
  59. React.useEffect(() => {
  60. if (!visible) {
  61. setSelectedFiles([]);
  62. }
  63. }, [visible]);
  64. const handleSelectFile = (file: FileType) => {
  65. if (!file) {
  66. console.error('No file provided to handleSelectFile');
  67. return;
  68. }
  69. if (multiple) {
  70. // 多选模式
  71. const newSelection = selectedFiles.some(f => f.id === file.id)
  72. ? selectedFiles.filter(f => f.id !== file.id)
  73. : [...selectedFiles, file];
  74. setSelectedFiles(newSelection);
  75. } else {
  76. // 单选模式
  77. setSelectedFiles([file]);
  78. }
  79. };
  80. const handleConfirm = () => {
  81. if (selectedFiles.length === 0) {
  82. message.warning('请先选择文件');
  83. return;
  84. }
  85. if (multiple) {
  86. // 多选模式
  87. (onSelect as MultipleFileSelectorProps['onSelect'])(selectedFiles);
  88. } else {
  89. // 单选模式
  90. (onSelect as SingleFileSelectorProps['onSelect'])(selectedFiles[0]);
  91. }
  92. onCancel();
  93. };
  94. const handleUploadSuccess = (fileKey: string, fileUrl: string, file: any) => {
  95. message.success('文件上传成功!请从列表中选择新上传的文件');
  96. // 刷新文件列表
  97. queryClient.invalidateQueries({ queryKey: ['files-for-selection', accept] });
  98. };
  99. const filteredFiles = Array.isArray(filesData?.data)
  100. ? filesData.data.filter((f: any) => !accept || f?.type?.startsWith(accept.replace('*', '')))
  101. : [];
  102. const isFileSelected = (file: FileType) => {
  103. return selectedFiles.some(f => f.id === file.id);
  104. };
  105. const getSelectionText = () => {
  106. if (multiple) {
  107. return selectedFiles.length > 0 ? `已选择 ${selectedFiles.length} 个文件` : '请选择文件';
  108. }
  109. return selectedFiles.length > 0 ? `已选择: ${selectedFiles[0].name}` : '请选择文件';
  110. };
  111. return (
  112. <Modal
  113. title="选择文件"
  114. open={visible}
  115. onCancel={onCancel}
  116. width={800}
  117. onOk={handleConfirm}
  118. okText="确认选择"
  119. cancelText="取消"
  120. okButtonProps={{ disabled: selectedFiles.length === 0 }}
  121. >
  122. <div style={{ marginBottom: 16 }}>
  123. <MinioUploader
  124. uploadPath={uploadPath}
  125. accept={accept}
  126. maxSize={maxSize}
  127. onUploadSuccess={handleUploadSuccess}
  128. buttonText={uploadButtonText}
  129. />
  130. <span style={{ color: '#666', fontSize: '12px', marginLeft: 8 }}>
  131. 上传后请从下方列表中选择文件
  132. </span>
  133. </div>
  134. <div style={{ marginBottom: 16 }}>
  135. <div style={{
  136. padding: '8px 12px',
  137. backgroundColor: '#f6ffed',
  138. border: '1px solid #b7eb8f',
  139. borderRadius: '4px',
  140. color: '#52c41a'
  141. }}>
  142. {getSelectionText()}
  143. </div>
  144. </div>
  145. <div style={{ maxHeight: 400, overflowY: 'auto' }}>
  146. {isLoading ? (
  147. <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
  148. 加载中...
  149. </div>
  150. ) : filteredFiles.length > 0 ? (
  151. filteredFiles.map((file) => (
  152. <div
  153. key={file.id}
  154. style={{
  155. display: 'flex',
  156. alignItems: 'center',
  157. padding: '12px',
  158. border: `1px solid ${isFileSelected(file) ? '#1890ff' : '#f0f0f0'}`,
  159. borderRadius: '4px',
  160. marginBottom: '8px',
  161. cursor: 'pointer',
  162. transition: 'all 0.3s',
  163. backgroundColor: isFileSelected(file) ? '#e6f7ff' : ''
  164. }}
  165. onClick={() => handleSelectFile(file)}
  166. onMouseEnter={(e) => {
  167. if (!isFileSelected(file)) {
  168. e.currentTarget.style.backgroundColor = '#f6ffed';
  169. e.currentTarget.style.borderColor = '#b7eb8f';
  170. }
  171. }}
  172. onMouseLeave={(e) => {
  173. if (!isFileSelected(file)) {
  174. e.currentTarget.style.backgroundColor = '';
  175. e.currentTarget.style.borderColor = '#f0f0f0';
  176. }
  177. }}
  178. >
  179. <Image
  180. src={file.fullUrl}
  181. alt={file.name}
  182. style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4, marginRight: 12 }}
  183. preview={false}
  184. />
  185. <div style={{ flex: 1 }}>
  186. <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>{file.name}</div>
  187. <div style={{ color: '#666', fontSize: '12px' }}>
  188. ID: {file.id} | 大小: {((file.size || 0) / 1024 / 1024).toFixed(2)}MB
  189. </div>
  190. </div>
  191. <Button
  192. type={isFileSelected(file) ? 'primary' : 'default'}
  193. size="small"
  194. >
  195. {isFileSelected(file) ? '已选择' : '选择'}
  196. </Button>
  197. </div>
  198. ))
  199. ) : (
  200. <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
  201. 暂无符合条件的文件,请先上传
  202. </div>
  203. )}
  204. </div>
  205. </Modal>
  206. );
  207. };
  208. // 类型守卫函数
  209. function isMultipleSelector(props: FileSelectorProps): props is MultipleFileSelectorProps {
  210. return props.multiple === true;
  211. }
  212. function isSingleSelector(props: FileSelectorProps): props is SingleFileSelectorProps {
  213. return props.multiple === false || props.multiple === undefined;
  214. }
  215. export default FileSelector;