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