import React, { useState, useEffect, useCallback } from 'react'; import { Card, Button, Space, Modal, message, Typography, Row, Col, Image, Form, Input, Avatar, Tag, Tooltip, Spin, Empty, Statistic, Select, Grid } from 'antd'; import { UserOutlined, ReloadOutlined, PlayCircleOutlined, EditOutlined, TeamOutlined, MergeCellsOutlined, ExclamationCircleOutlined, EyeOutlined, HeartOutlined, SearchOutlined } from '@ant-design/icons'; import { getMyFaceClusters, updateMyCluster, startMyFaceClustering, mergeMyUserClusters, getMyPicturesByCluster, type FaceClusterResponse, type UpdateClusterRequest } from '../../api'; import type { PictureResponse } from '../../api/pictureApi'; const { Title, Text, Paragraph } = Typography; const { confirm } = Modal; const { useBreakpoint } = Grid; const FaceExplore: React.FC = () => { const screens = useBreakpoint(); const isMobile = !screens.md; const [clusters, setClusters] = useState([]); const [loading, setLoading] = useState(false); const [clusteringLoading, setClusteringLoading] = useState(false); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [pageSize] = useState(12); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [isMergeModalVisible, setIsMergeModalVisible] = useState(false); const [isPictureModalVisible, setIsPictureModalVisible] = useState(false); const [editingCluster, setEditingCluster] = useState(null); const [targetCluster, setTargetCluster] = useState(null); const [clusterPictures, setClusterPictures] = useState([]); const [picturesLoading, setPicturesLoading] = useState(false); const [editForm] = Form.useForm(); const [mergeForm] = Form.useForm(); const fetchClusters = useCallback(async (page = currentPage) => { setLoading(true); try { const response = await getMyFaceClusters(page, pageSize); if (response.success) { const actualData = response.data?.data || response.data; setClusters(Array.isArray(actualData) ? actualData : []); setTotal(response.data?.totalCount || response.totalCount || 0); } else { message.error(response.message || '获取人脸聚类失败'); setClusters([]); setTotal(0); } } catch (error) { message.error('获取人脸聚类失败,请检查网络连接'); setClusters([]); setTotal(0); } finally { setLoading(false); } }, [currentPage, pageSize]); const fetchClusterPictures = useCallback(async (clusterId: number) => { setPicturesLoading(true); try { const response = await getMyPicturesByCluster(clusterId, 1, 50); if (response.success) { const actualData = response.data?.data || response.data; setClusterPictures(Array.isArray(actualData) ? actualData : []); } else { message.error(response.message || '获取聚类图片失败'); setClusterPictures([]); } } catch (error) { message.error('获取聚类图片失败'); setClusterPictures([]); } finally { setPicturesLoading(false); } }, []); useEffect(() => { fetchClusters(); }, [fetchClusters]); const handleLoadMore = () => { const nextPage = currentPage + 1; setCurrentPage(nextPage); fetchClusters(nextPage); }; const showEditModal = (cluster: FaceClusterResponse) => { setEditingCluster(cluster); editForm.setFieldsValue({ personName: cluster.personName, description: cluster.description, }); setIsEditModalVisible(true); }; const showMergeModal = (cluster: FaceClusterResponse) => { setTargetCluster(cluster); mergeForm.resetFields(); setIsMergeModalVisible(true); }; const showPicturesModal = (cluster: FaceClusterResponse) => { setEditingCluster(cluster); setIsPictureModalVisible(true); fetchClusterPictures(cluster.id); }; const handleEditOk = async () => { if (!editingCluster) return; try { const values = await editForm.validateFields(); setLoading(true); const response = await updateMyCluster(editingCluster.id, values); if (response.success) { message.success('更新聚类信息成功'); setIsEditModalVisible(false); fetchClusters(); } else { message.error(response.message || '更新失败'); } } catch (errorInfo) { console.log('Validate Failed:', errorInfo); } finally { setLoading(false); } }; const handleMergeOk = async () => { if (!targetCluster) return; try { const values = await mergeForm.validateFields(); setLoading(true); const response = await mergeMyUserClusters(targetCluster.id, values.sourceClusterId); if (response.success) { message.success('合并聚类成功'); setIsMergeModalVisible(false); fetchClusters(); } else { message.error(response.message || '合并失败'); } } catch (errorInfo) { console.log('Validate Failed:', errorInfo); } finally { setLoading(false); } }; const handleStartClustering = () => { confirm({ title: '开始人脸聚类分析', icon: , content: '这将分析您上传图片中的未分类人脸,帮助您找到相同的人物。可能需要一些时间,确定要开始吗?', async onOk() { setClusteringLoading(true); try { const response = await startMyFaceClustering(); if (response.success) { message.success('人脸聚类任务已开始,请稍后刷新查看结果'); } else { message.error(response.message || '启动聚类失败'); } } catch (error) { message.error('启动聚类失败'); } finally { setClusteringLoading(false); } } }); }; const renderClusterCard = (cluster: FaceClusterResponse) => ( {cluster.thumbnailPath ? ( ) : ( } style={{ backgroundColor: '#e6f7ff', border: '3px solid #fff', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }} /> )} } actions={[ showPicturesModal(cluster)} /> , showEditModal(cluster)} /> , showMergeModal(cluster)} /> ]} > {cluster.name} {cluster.faceCount} 张人脸 } description={
{cluster.personName && ( {cluster.personName} )} {cluster.description && ( {cluster.description} )} 最后更新: {new Date(cluster.lastUpdatedAt).toLocaleDateString()}
} />
); return (
{/* 页面头部 */}
人脸探索 发现和管理您照片中的人物,为每个聚类命名和整理
{/* 统计信息 */} } valueStyle={{ color: '#1890ff' }} /> c.personName).length} prefix={} valueStyle={{ color: '#52c41a' }} /> sum + c.faceCount, 0)} prefix={} valueStyle={{ color: '#722ed1' }} /> {/* 聚类网格 */} {clusters.length > 0 ? ( <> {clusters.map(cluster => ( {renderClusterCard(cluster)} ))} {/* 加载更多按钮 */} {clusters.length < total && (
)} ) : ( 还没有发现人脸聚类 点击"开始聚类分析"来分析您的照片并发现其中的人物
} image={Empty.PRESENTED_IMAGE_SIMPLE} > )} {/* 编辑聚类模态框 */} setIsEditModalVisible(false)} confirmLoading={loading} destroyOnClose >
{/* 合并聚类模态框 */} setIsMergeModalVisible(false)} confirmLoading={loading} destroyOnClose >