| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- 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;
|