Files
timeline-frontend/src/pages/gallery/components/GridView.tsx

205 lines
6.3 KiB
TypeScript
Raw Normal View History

// src/pages/gallery\components\GridView.tsx
2025-08-04 16:56:39 +08:00
import { ImageItem } from '@/pages/gallery/typings';
import { formatDuration } from '@/utils/timelineUtils';
import { getAuthization } from '@/utils/userUtils';
import {
DeleteOutlined,
DownloadOutlined,
EyeOutlined,
MoreOutlined,
PlayCircleOutlined,
} from '@ant-design/icons';
2025-08-04 16:56:39 +08:00
import { Button, Checkbox, Dropdown, Menu, Spin } from 'antd';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { useIsMobile } from '@/hooks/useIsMobile';
2025-08-04 16:56:39 +08:00
import '../index.css';
interface GridViewProps {
imageList: ImageItem[];
viewMode: 'small' | 'large' | 'list' | 'table';
batchMode: boolean;
selectedRowKeys: string[];
onPreview: (index: number) => void;
onSelect: (instanceId: string, checked: boolean) => void;
onDownload: (instanceId: string, imageName: string) => void;
onDelete: (instanceId: string, imageName: string) => void;
loadingMore?: boolean;
onScroll: (e: React.UIEvent<HTMLDivElement>) => void;
}
const GridView: FC<GridViewProps> = ({
imageList,
viewMode,
batchMode,
selectedRowKeys,
onPreview,
onSelect,
onDownload,
onDelete,
loadingMore,
onScroll,
}) => {
const isMobile = useIsMobile();
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
2025-08-04 16:56:39 +08:00
const getImageSize = useCallback(() => {
if (isMobile) {
// Mobile: 3 columns for small, 2 columns for large
const padding = 16; // page padding
const gap = 8; // grid gap
if (viewMode === 'large') {
const size = (windowWidth - padding * 2 - gap) / 2;
return { width: size, height: size };
}
// default small
const size = (windowWidth - padding * 2 - gap * 2) / 3;
return { width: size, height: size };
}
2025-08-04 16:56:39 +08:00
switch (viewMode) {
case 'small':
return { width: 150, height: 150 };
case 'large':
return { width: 300, height: 300 };
default:
return { width: 150, height: 150 };
}
}, [viewMode, isMobile, windowWidth]);
2025-08-04 16:56:39 +08:00
const imageSize = getImageSize();
const getImageMenu = useCallback(
(item: ImageItem) => (
<Menu>
<Menu.Item
key="preview"
icon={<EyeOutlined />}
onClick={() => {
const index = imageList.findIndex((img) => img.instanceId === item.instanceId);
onPreview(index);
}}
>
</Menu.Item>
<Menu.Item
key="download"
icon={<DownloadOutlined />}
onClick={() => onDownload(item.instanceId, item.imageName)}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
key="delete"
icon={<DeleteOutlined />}
danger
onClick={() => onDelete(item.instanceId, item.imageName)}
>
</Menu.Item>
</Menu>
),
[imageList, onPreview, onDownload, onDelete],
);
// 根据视图模式确定图像 URL
const getImageUrl = (item: ImageItem, isHighRes?: boolean) => {
// 如果是视频,使用封面图
if (item.thumbnailInstanceId) {
return `/file/image-low-res/${item.thumbnailInstanceId}?Authorization=${getAuthization()}`;
}
// 小图模式使用低分辨率图像,除非明确要求高清
if (viewMode === 'small' && !isHighRes) {
return `/file/image-low-res/${item.instanceId}?Authorization=${getAuthization()}`;
}
// 其他模式使用原图
return `/file/image/${item.instanceId}?Authorization=${getAuthization()}`;
};
2025-08-04 16:56:39 +08:00
return (
<div
className={viewMode === 'small' ? 'small-grid-view' : 'large-grid-view'}
onScroll={onScroll}
style={isMobile ? {
display: 'grid',
gridTemplateColumns: viewMode === 'large' ? 'repeat(2, 1fr)' : 'repeat(3, 1fr)',
gap: '8px',
paddingBottom: '20px'
} : {}}
2025-08-04 16:56:39 +08:00
>
{imageList.map((item: ImageItem, index: number) => (
<div
key={item.instanceId}
className="image-card"
style={isMobile ? { width: '100%', margin: 0 } : {}}
>
2025-08-04 16:56:39 +08:00
{batchMode && (
<Checkbox
className="image-checkbox"
checked={selectedRowKeys.includes(item.instanceId)}
onChange={(e) => onSelect(item.instanceId, e.target.checked)}
/>
)}
<div
className="image-wrapper"
style={{
width: isMobile ? '100%' : imageSize.width,
2025-08-04 16:56:39 +08:00
height: imageSize.height,
backgroundImage: `url(${getImageUrl(item, false)})`,
2025-08-04 16:56:39 +08:00
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
2025-08-04 16:56:39 +08:00
}}
onClick={() => !batchMode && onPreview(index)}
>
{(item.duration || item.thumbnailInstanceId) && (
<PlayCircleOutlined style={{ fontSize: isMobile ? '24px' : '32px', color: 'rgba(255,255,255,0.8)' }} />
)}
{item.duration && (
<span
style={{
position: 'absolute',
bottom: 4,
right: 4,
background: 'rgba(0,0,0,0.5)',
color: '#fff',
padding: '2px 4px',
borderRadius: 2,
fontSize: 10,
}}
>
{formatDuration(item.duration)}
</span>
)}
</div>
2025-08-04 16:56:39 +08:00
<div className="image-info">
<div className="image-title" title={item.imageName}>
{item.imageName}
</div>
<Dropdown overlay={getImageMenu(item)} trigger={['click']}>
<Button type="text" icon={<MoreOutlined />} size={isMobile ? "small" : "middle"} />
2025-08-04 16:56:39 +08:00
</Dropdown>
</div>
</div>
))}
{loadingMore && (
<div style={{ gridColumn: '1 / -1', textAlign: 'center', padding: '20px' }}>
<Spin />
</div>
)}
</div>
);
};
export default GridView;