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

184 lines
5.0 KiB
TypeScript
Raw Normal View History

/**
* PhotoCard Component
* Feature: personal-user-enhancements
*
* Individual photo card with reactions support
*/
import React from 'react';
import { Button, Checkbox, Dropdown, Menu } from 'antd';
import {
DeleteOutlined,
DownloadOutlined,
EyeOutlined,
MoreOutlined,
PlayCircleOutlined,
} from '@ant-design/icons';
import { ReactionBar } from '@/components/Reactions';
import useReactions from '@/hooks/useReactions';
import { ImageItem } from '@/pages/gallery/typings';
import { formatDuration } from '@/utils/timelineUtils';
import { getAuthization } from '@/utils/userUtils';
interface PhotoCardProps {
item: ImageItem;
index: number;
viewMode: 'small' | 'large';
batchMode: boolean;
isSelected: boolean;
imageSize: { width: number | string; height: number };
isMobile: boolean;
onPreview: (index: number) => void;
onSelect: (instanceId: string, checked: boolean) => void;
onDownload: (instanceId: string, imageName: string) => void;
onDelete: (instanceId: string, imageName: string) => void;
}
const PhotoCard: React.FC<PhotoCardProps> = ({
item,
index,
viewMode,
batchMode,
isSelected,
imageSize,
isMobile,
onPreview,
onSelect,
onDownload,
onDelete,
}) => {
// Initialize reactions for this photo
const {
reactions,
addReaction,
updateReaction,
removeReaction,
actionLoading: reactionLoading,
} = useReactions('photo', item.instanceId, {
autoFetch: true,
autoSubscribe: true,
});
// 根据视图模式确定图像 URL
const getImageUrl = (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()}`;
};
const getImageMenu = () => (
<Menu>
<Menu.Item
key="preview"
icon={<EyeOutlined />}
onClick={() => 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>
);
return (
<div
key={item.instanceId}
className="image-card"
style={isMobile ? { width: '100%', margin: 0 } : {}}
>
{batchMode && (
<Checkbox
className="image-checkbox"
checked={isSelected}
onChange={(e) => onSelect(item.instanceId, e.target.checked)}
/>
)}
<div
className="image-wrapper"
style={{
width: isMobile ? '100%' : imageSize.width,
height: imageSize.height,
backgroundImage: `url(${getImageUrl(false)})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
}}
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>
<div className="image-info">
<div className="image-title" title={item.imageName}>
{item.imageName}
</div>
<Dropdown overlay={getImageMenu()} trigger={['click']}>
<Button type="text" icon={<MoreOutlined />} size={isMobile ? "small" : "middle"} />
</Dropdown>
</div>
{/* Reactions */}
<div
className="image-reactions"
onClick={(e) => e.stopPropagation()}
style={{ padding: '4px 8px' }}
>
<ReactionBar
entityType="photo"
entityId={item.instanceId}
reactionSummary={reactions || undefined}
onAdd={addReaction}
onRemove={removeReaction}
onChange={updateReaction}
loading={reactionLoading}
size="small"
showPicker={false}
/>
</div>
</div>
);
};
export default PhotoCard;