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