184 lines
5.0 KiB
TypeScript
184 lines
5.0 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|