|
|
@@ -0,0 +1,229 @@
|
|
|
+import React from 'react';
|
|
|
+import { Image, Spin, Empty } from 'antd';
|
|
|
+import { EyeOutlined, FileImageOutlined } from '@ant-design/icons';
|
|
|
+import { useQuery } from '@tanstack/react-query';
|
|
|
+import { fileClient } from '@/client/api';
|
|
|
+import type { InferResponseType } from 'hono/client';
|
|
|
+
|
|
|
+// 定义文件类型
|
|
|
+type FileItem = InferResponseType<typeof fileClient[':id']['$get'], 200>;
|
|
|
+
|
|
|
+interface FilePreviewProps {
|
|
|
+ fileIds?: number[];
|
|
|
+ files?: any[];
|
|
|
+ maxCount?: number;
|
|
|
+ size?: 'small' | 'medium' | 'large';
|
|
|
+ showCount?: boolean;
|
|
|
+ onFileClick?: (file: FileItem) => void;
|
|
|
+}
|
|
|
+
|
|
|
+interface FilePreviewItemProps {
|
|
|
+ file: FileItem;
|
|
|
+ size: 'small' | 'medium' | 'large';
|
|
|
+ index?: number;
|
|
|
+ total?: number;
|
|
|
+}
|
|
|
+
|
|
|
+const FilePreviewItem: React.FC<FilePreviewItemProps> = ({ file, size, index, total }) => {
|
|
|
+ const getSize = () => {
|
|
|
+ switch (size) {
|
|
|
+ case 'small':
|
|
|
+ return { width: 45, height: 45 };
|
|
|
+ case 'medium':
|
|
|
+ return { width: 80, height: 80 };
|
|
|
+ case 'large':
|
|
|
+ return { width: 120, height: 120 };
|
|
|
+ default:
|
|
|
+ return { width: 80, height: 80 };
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const { width, height } = getSize();
|
|
|
+
|
|
|
+ const isImage = file.type?.startsWith('image/');
|
|
|
+ const previewText = isImage ? '预览' : '查看';
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ position: 'relative',
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ border: '1px solid #d9d9d9',
|
|
|
+ borderRadius: 4,
|
|
|
+ overflow: 'hidden',
|
|
|
+ backgroundColor: '#fafafa',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {isImage ? (
|
|
|
+ <Image
|
|
|
+ src={file.fullUrl}
|
|
|
+ alt={file.name}
|
|
|
+ style={{
|
|
|
+ width: '100%',
|
|
|
+ height: '100%',
|
|
|
+ objectFit: 'cover',
|
|
|
+ }}
|
|
|
+ preview={{
|
|
|
+ mask: (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ color: 'white',
|
|
|
+ backgroundColor: 'rgba(0,0,0,0.7)',
|
|
|
+ height: '100%',
|
|
|
+ fontSize: size === 'small' ? 10 : 12,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <EyeOutlined style={{ fontSize: size === 'small' ? 14 : 16 }} />
|
|
|
+ <span style={{ marginTop: 2 }}>{previewText}</span>
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ height: '100%',
|
|
|
+ color: '#666',
|
|
|
+ fontSize: size === 'small' ? 10 : 12,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <FileImageOutlined style={{ fontSize: size === 'small' ? 16 : 20, marginBottom: 2 }} />
|
|
|
+ <span style={{ textAlign: 'center', lineHeight: 1.2 }}>
|
|
|
+ {file.name.length > 8 ? `${file.name.substring(0, 6)}...` : file.name}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 序号标记 */}
|
|
|
+ {index !== undefined && total !== undefined && total > 1 && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ top: 0,
|
|
|
+ right: 0,
|
|
|
+ backgroundColor: 'rgba(0,0,0,0.5)',
|
|
|
+ color: 'white',
|
|
|
+ fontSize: 10,
|
|
|
+ padding: '2px 4px',
|
|
|
+ borderRadius: '0 0 0 4px',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {index + 1}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const FilePreview: React.FC<FilePreviewProps> = ({
|
|
|
+ fileIds = [],
|
|
|
+ files = [],
|
|
|
+ maxCount = 6,
|
|
|
+ size = 'medium',
|
|
|
+ showCount = true,
|
|
|
+ onFileClick,
|
|
|
+}) => {
|
|
|
+ // 合并文件ID和文件对象
|
|
|
+ const allFileIds = [...fileIds, ...(files?.map(f => f.id) || [])];
|
|
|
+ const uniqueFileIds = [...new Set(allFileIds)].filter(Boolean);
|
|
|
+
|
|
|
+ // 使用 React Query 查询文件详情
|
|
|
+ const { data: fileDetails, isLoading, error } = useQuery({
|
|
|
+ queryKey: ['files', uniqueFileIds],
|
|
|
+ queryFn: async () => {
|
|
|
+ if (uniqueFileIds.length === 0) return [];
|
|
|
+
|
|
|
+ const promises = uniqueFileIds.map(async (id) => {
|
|
|
+ try {
|
|
|
+ const response = await fileClient[':id']['$get']({ param: { id: id.toString() } });
|
|
|
+ if (response.ok) {
|
|
|
+ return response.json();
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`获取文件 ${id} 详情失败:`, error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const results = await Promise.all(promises);
|
|
|
+ return results.filter(Boolean) as FileItem[];
|
|
|
+ },
|
|
|
+ enabled: uniqueFileIds.length > 0,
|
|
|
+ staleTime: 5 * 60 * 1000, // 5分钟
|
|
|
+ gcTime: 10 * 60 * 1000, // 10分钟
|
|
|
+ });
|
|
|
+
|
|
|
+ if (isLoading) {
|
|
|
+ return (
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'center', padding: 20 }}>
|
|
|
+ <Spin tip="加载图片中..." />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error) {
|
|
|
+ return (
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'center', padding: 20 }}>
|
|
|
+ <Empty description="加载图片失败" />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const displayFiles = fileDetails?.slice(0, maxCount) || [];
|
|
|
+ const remainingCount = Math.max(0, (fileDetails?.length || 0) - maxCount);
|
|
|
+
|
|
|
+ if (displayFiles.length === 0) {
|
|
|
+ return (
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'center', padding: 10 }}>
|
|
|
+ <Empty description="暂无图片" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ display: 'flex',
|
|
|
+ flexWrap: 'wrap',
|
|
|
+ gap: 8,
|
|
|
+ alignItems: 'flex-start',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {displayFiles.map((file, index) => (
|
|
|
+ <FilePreviewItem
|
|
|
+ key={file.id}
|
|
|
+ file={file}
|
|
|
+ size={size}
|
|
|
+ index={index}
|
|
|
+ total={displayFiles.length}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {showCount && remainingCount > 0 && (
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ marginTop: 8,
|
|
|
+ fontSize: 12,
|
|
|
+ color: '#666',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 还有 {remainingCount} 张图片未显示
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default FilePreview;
|