FilePreview.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import React from 'react';
  2. import { Image, Spin, Empty } from 'antd';
  3. import { EyeOutlined, FileImageOutlined } from '@ant-design/icons';
  4. import { useQuery } from '@tanstack/react-query';
  5. import { fileClient } from '@/client/api';
  6. import type { InferResponseType } from 'hono/client';
  7. // 定义文件类型
  8. type FileItem = InferResponseType<typeof fileClient[':id']['$get'], 200>;
  9. interface FilePreviewProps {
  10. fileIds?: number[];
  11. files?: any[];
  12. maxCount?: number;
  13. size?: 'small' | 'medium' | 'large';
  14. showCount?: boolean;
  15. onFileClick?: (file: FileItem) => void;
  16. }
  17. interface FilePreviewItemProps {
  18. file: FileItem;
  19. size: 'small' | 'medium' | 'large';
  20. index?: number;
  21. total?: number;
  22. }
  23. const FilePreviewItem: React.FC<FilePreviewItemProps> = ({ file, size, index, total }) => {
  24. const getSize = () => {
  25. switch (size) {
  26. case 'small':
  27. return { width: 45, height: 45 };
  28. case 'medium':
  29. return { width: 80, height: 80 };
  30. case 'large':
  31. return { width: 120, height: 120 };
  32. default:
  33. return { width: 80, height: 80 };
  34. }
  35. };
  36. const { width, height } = getSize();
  37. const isImage = file.type?.startsWith('image/');
  38. const previewText = isImage ? '预览' : '查看';
  39. return (
  40. <div
  41. style={{
  42. position: 'relative',
  43. width,
  44. height,
  45. border: '1px solid #d9d9d9',
  46. borderRadius: 4,
  47. overflow: 'hidden',
  48. backgroundColor: '#fafafa',
  49. }}
  50. >
  51. {isImage ? (
  52. <Image
  53. src={file.fullUrl}
  54. alt={file.name}
  55. style={{
  56. width: '100%',
  57. height: '100%',
  58. objectFit: 'cover',
  59. }}
  60. preview={{
  61. mask: (
  62. <div
  63. style={{
  64. display: 'flex',
  65. flexDirection: 'column',
  66. alignItems: 'center',
  67. justifyContent: 'center',
  68. color: 'white',
  69. backgroundColor: 'rgba(0,0,0,0.7)',
  70. height: '100%',
  71. fontSize: size === 'small' ? 10 : 12,
  72. }}
  73. >
  74. <EyeOutlined style={{ fontSize: size === 'small' ? 14 : 16 }} />
  75. <span style={{ marginTop: 2 }}>{previewText}</span>
  76. </div>
  77. ),
  78. }}
  79. />
  80. ) : (
  81. <div
  82. style={{
  83. display: 'flex',
  84. flexDirection: 'column',
  85. alignItems: 'center',
  86. justifyContent: 'center',
  87. height: '100%',
  88. color: '#666',
  89. fontSize: size === 'small' ? 10 : 12,
  90. }}
  91. >
  92. <FileImageOutlined style={{ fontSize: size === 'small' ? 16 : 20, marginBottom: 2 }} />
  93. <span style={{ textAlign: 'center', lineHeight: 1.2 }}>
  94. {file.name.length > 8 ? `${file.name.substring(0, 6)}...` : file.name}
  95. </span>
  96. </div>
  97. )}
  98. {/* 序号标记 */}
  99. {index !== undefined && total !== undefined && total > 1 && (
  100. <div
  101. style={{
  102. position: 'absolute',
  103. top: 0,
  104. right: 0,
  105. backgroundColor: 'rgba(0,0,0,0.5)',
  106. color: 'white',
  107. fontSize: 10,
  108. padding: '2px 4px',
  109. borderRadius: '0 0 0 4px',
  110. }}
  111. >
  112. {index + 1}
  113. </div>
  114. )}
  115. </div>
  116. );
  117. };
  118. const FilePreview: React.FC<FilePreviewProps> = ({
  119. fileIds = [],
  120. files = [],
  121. maxCount = 6,
  122. size = 'medium',
  123. showCount = true,
  124. onFileClick,
  125. }) => {
  126. // 合并文件ID和文件对象
  127. const allFileIds = [...fileIds, ...(files?.map(f => f.id) || [])];
  128. const uniqueFileIds = [...new Set(allFileIds)].filter(Boolean);
  129. // 使用 React Query 查询文件详情
  130. const { data: fileDetails, isLoading, error } = useQuery({
  131. queryKey: ['files', uniqueFileIds],
  132. queryFn: async () => {
  133. if (uniqueFileIds.length === 0) return [];
  134. const promises = uniqueFileIds.map(async (id) => {
  135. try {
  136. const response = await fileClient[':id']['$get']({ param: { id: id.toString() } });
  137. if (response.ok) {
  138. return response.json();
  139. }
  140. return null;
  141. } catch (error) {
  142. console.error(`获取文件 ${id} 详情失败:`, error);
  143. return null;
  144. }
  145. });
  146. const results = await Promise.all(promises);
  147. return results.filter(Boolean) as FileItem[];
  148. },
  149. enabled: uniqueFileIds.length > 0,
  150. staleTime: 5 * 60 * 1000, // 5分钟
  151. gcTime: 10 * 60 * 1000, // 10分钟
  152. });
  153. if (isLoading) {
  154. return (
  155. <div style={{ display: 'flex', justifyContent: 'center', padding: 20 }}>
  156. <Spin tip="加载图片中..." />
  157. </div>
  158. );
  159. }
  160. if (error) {
  161. return (
  162. <div style={{ display: 'flex', justifyContent: 'center', padding: 20 }}>
  163. <Empty description="加载图片失败" />
  164. </div>
  165. );
  166. }
  167. const displayFiles = fileDetails?.slice(0, maxCount) || [];
  168. const remainingCount = Math.max(0, (fileDetails?.length || 0) - maxCount);
  169. if (displayFiles.length === 0) {
  170. return (
  171. <div style={{ display: 'flex', justifyContent: 'center', padding: 10 }}>
  172. <Empty description="暂无图片" image={Empty.PRESENTED_IMAGE_SIMPLE} />
  173. </div>
  174. );
  175. }
  176. return (
  177. <div>
  178. <div
  179. style={{
  180. display: 'flex',
  181. flexWrap: 'wrap',
  182. gap: 8,
  183. alignItems: 'flex-start',
  184. }}
  185. >
  186. {displayFiles.map((file, index) => (
  187. <FilePreviewItem
  188. key={file.id}
  189. file={file}
  190. size={size}
  191. index={index}
  192. total={displayFiles.length}
  193. />
  194. ))}
  195. </div>
  196. {showCount && remainingCount > 0 && (
  197. <div
  198. style={{
  199. marginTop: 8,
  200. fontSize: 12,
  201. color: '#666',
  202. }}
  203. >
  204. 还有 {remainingCount} 张图片未显示
  205. </div>
  206. )}
  207. </div>
  208. );
  209. };
  210. export default FilePreview;