Files
timeline-frontend/src/pages/albums/components/PhotoSelector.tsx
jhao 5a0aa2b3c1
All checks were successful
test/timeline-frontend/pipeline/head This commit looks good
feat: 添加评论、反应、离线编辑及主题定制功能
- 实现评论系统,包括评论输入、列表展示和集成指南
- 添加反应功能组件(ReactionBar、ReactionButton、ReactionPicker)
- 实现离线编辑支持,包括同步状态管理和冲突解决
- 添加主题定制功能,支持多种配色方案和主题预览
- 新增多视图布局选项(时间线、分组、砌体视图)
- 实现个人资料编辑器,支持头像、简介和自定义字段编辑
- 添加统计页面,展示存储使用情况和上传趋势
- 新增相册管理功能,支持相册创建、编辑和照片管理
- 实现响应式设计和加载骨架屏组件
- 扩展国际化支持,新增孟加拉语、波斯语、印尼语、日语、葡萄牙语等语言
- 添加错误边界组件和离线指示器
- 更新配置文件、路由和依赖项
- 新增完整的文档、测试用例和集成指南
2026-02-25 15:02:05 +08:00

170 lines
4.5 KiB
TypeScript

/**
* Photo Selector Component with Multi-Select
* Feature: personal-user-enhancements
* Requirements: 2.2
*/
import React, { useState, useEffect } from 'react';
import { Button, message, Spin, Empty, Checkbox } from 'antd';
import { useModel, request } from '@umijs/max';
import styles from '../index.less';
interface PhotoSelectorProps {
albumId: string;
existingPhotoIds: string[];
onSuccess: () => void;
onCancel: () => void;
}
interface Photo {
id: string;
url: string;
thumbnailUrl: string;
}
const PhotoSelector: React.FC<PhotoSelectorProps> = ({
albumId,
existingPhotoIds,
onSuccess,
onCancel,
}) => {
const { addPhotosToAlbum } = useModel('albums');
const [photos, setPhotos] = useState<Photo[]>([]);
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [selectedPhotoIds, setSelectedPhotoIds] = useState<string[]>([]);
useEffect(() => {
fetchUserPhotos();
}, []);
const fetchUserPhotos = async () => {
setLoading(true);
try {
// Fetch user's photos from gallery
const response = await request('/api/v1/gallery/photos', {
method: 'GET',
params: { pageSize: 100 },
});
// Filter out photos already in the album
const availablePhotos = response.items?.filter(
(photo: Photo) => !existingPhotoIds.includes(photo.id)
) || [];
setPhotos(availablePhotos);
} catch (error) {
message.error('Failed to load photos');
console.error('Error loading photos:', error);
} finally {
setLoading(false);
}
};
const handlePhotoToggle = (photoId: string) => {
setSelectedPhotoIds((prev) =>
prev.includes(photoId)
? prev.filter((id) => id !== photoId)
: [...prev, photoId]
);
};
const handleSelectAll = () => {
if (selectedPhotoIds.length === photos.length) {
setSelectedPhotoIds([]);
} else {
setSelectedPhotoIds(photos.map((p) => p.id));
}
};
const handleSubmit = async () => {
if (selectedPhotoIds.length === 0) {
message.warning('Please select at least one photo');
return;
}
setSubmitting(true);
try {
await addPhotosToAlbum(albumId, selectedPhotoIds);
message.success(`${selectedPhotoIds.length} photo(s) added to album`);
onSuccess();
} catch (error) {
message.error('Failed to add photos to album');
console.error('Error adding photos:', error);
} finally {
setSubmitting(false);
}
};
if (loading) {
return <Spin spinning={true} />;
}
if (photos.length === 0) {
return (
<Empty
description="No photos available to add"
image={Empty.PRESENTED_IMAGE_SIMPLE}
>
<Button onClick={onCancel}>Back to Album</Button>
</Empty>
);
}
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<Checkbox
checked={selectedPhotoIds.length === photos.length}
indeterminate={selectedPhotoIds.length > 0 && selectedPhotoIds.length < photos.length}
onChange={handleSelectAll}
>
Select All ({selectedPhotoIds.length} selected)
</Checkbox>
</div>
<div>
<Button onClick={onCancel} style={{ marginRight: 8 }}>
Cancel
</Button>
<Button
type="primary"
onClick={handleSubmit}
loading={submitting}
disabled={selectedPhotoIds.length === 0}
>
Add {selectedPhotoIds.length > 0 ? `${selectedPhotoIds.length} ` : ''}Photo(s)
</Button>
</div>
</div>
<div className={styles.photoGrid}>
{photos.map((photo) => {
const isSelected = selectedPhotoIds.includes(photo.id);
return (
<div
key={photo.id}
className={`${styles.photoItem} ${isSelected ? styles.photoItemSelected : ''}`}
onClick={() => handlePhotoToggle(photo.id)}
>
<img src={photo.thumbnailUrl} alt="" />
<Checkbox
checked={isSelected}
style={{
position: 'absolute',
top: 8,
left: 8,
zIndex: 1,
}}
onClick={(e) => e.stopPropagation()}
/>
</div>
);
})}
</div>
</div>
);
};
export default PhotoSelector;