From 63e40091ac549591dcb9be7d69f4b0d70234d7ef Mon Sep 17 00:00:00 2001 From: imeepos Date: Mon, 28 Jul 2025 16:40:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B9=E6=A1=88=E5=AF=B9=E6=AF=94?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E9=9B=86=E6=88=90ImageCard=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=B9=B6=E5=AE=9E=E7=8E=B0=E6=9C=AC=E5=9C=B0=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将方案对比页面的素材展示替换为ImageCard组件 - 创建MaterialSearchResult到GroundingSource的数据适配器 - 实现本地文件下载功能,支持用户选择保存目录 - 添加文件扩展名自动识别和文件类型过滤 - 集成Tauri的文件保存对话框和下载命令 - 优化用户体验,提供完整的图片展示和下载功能 - 支持查看大图和本地保存功能 - 保持下拉加载更多和刷新功能的完整性 --- .../outfit/OutfitComparisonView.tsx | 388 ++++++++++++++++-- 1 file changed, 351 insertions(+), 37 deletions(-) diff --git a/apps/desktop/src/components/outfit/OutfitComparisonView.tsx b/apps/desktop/src/components/outfit/OutfitComparisonView.tsx index bcbb772..b7c7a6c 100644 --- a/apps/desktop/src/components/outfit/OutfitComparisonView.tsx +++ b/apps/desktop/src/components/outfit/OutfitComparisonView.tsx @@ -2,6 +2,11 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Heart, Search, Loader2, ArrowLeftRight, X, RefreshCw, RotateCcw } from 'lucide-react'; import { OutfitFavorite, CompareOutfitFavoritesResponse } from '../../types/outfitFavorite'; import { OutfitFavoriteService } from '../../services/outfitFavoriteService'; +import ImageCard from '../ImageCard'; +import { GroundingSource } from '../../types/ragGrounding'; +import { save } from '@tauri-apps/plugin-dialog'; +import { invoke } from '@tauri-apps/api/core'; + interface OutfitComparisonViewProps { /** 第一个收藏方案 */ @@ -31,6 +36,20 @@ export const OutfitComparisonView: React.FC = ({ const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + // 加载更多和刷新状态 + const [isLoadingMore1, setIsLoadingMore1] = useState(false); + const [isLoadingMore2, setIsLoadingMore2] = useState(false); + const [isRefreshing1, setIsRefreshing1] = useState(false); + const [isRefreshing2, setIsRefreshing2] = useState(false); + const [hasMoreData1, setHasMoreData1] = useState(true); + const [hasMoreData2, setHasMoreData2] = useState(true); + const [currentPage1, setCurrentPage1] = useState(1); + const [currentPage2, setCurrentPage2] = useState(1); + + // 滚动容器引用 + const scrollContainer1Ref = useRef(null); + const scrollContainer2Ref = useRef(null); + // 执行对比 const performComparison = useCallback(async () => { setIsLoading(true); @@ -64,6 +83,12 @@ export const OutfitComparisonView: React.FC = ({ setComparisonData(response); + // 设置分页状态 + setHasMoreData1(response.materials_1.current_page < response.materials_1.total_pages); + setHasMoreData2(response.materials_2.current_page < response.materials_2.total_pages); + setCurrentPage1(response.materials_1.current_page); + setCurrentPage2(response.materials_2.current_page); + // 清理临时收藏(可选,或者可以在组件卸载时清理) // 这里暂时保留,让用户可以看到完整的对比结果 } catch (tempError) { @@ -77,6 +102,12 @@ export const OutfitComparisonView: React.FC = ({ favorite2.id ); setComparisonData(response); + + // 设置分页状态 + setHasMoreData1(response.materials_1.current_page < response.materials_1.total_pages); + setHasMoreData2(response.materials_2.current_page < response.materials_2.total_pages); + setCurrentPage1(response.materials_1.current_page); + setCurrentPage2(response.materials_2.current_page); } } catch (err) { console.error('对比失败:', err); @@ -86,6 +117,220 @@ export const OutfitComparisonView: React.FC = ({ } }, [favorite1, favorite2, isTemporary]); + // 加载更多素材 - 左侧 + const loadMoreMaterials1 = useCallback(async () => { + if (!comparisonData || isLoadingMore1 || !hasMoreData1) return; + + setIsLoadingMore1(true); + try { + const nextPage = currentPage1 + 1; + const response = await OutfitFavoriteService.searchMaterialsByFavorite( + comparisonData.favorite_1.id, + nextPage, + 9 + ); + + setComparisonData(prev => prev ? { + ...prev, + materials_1: { + ...response, + results: [...prev.materials_1.results, ...response.results] + } + } : null); + + setCurrentPage1(nextPage); + setHasMoreData1(nextPage < response.total_pages); + } catch (error) { + console.error('加载更多素材失败 (左侧):', error); + } finally { + setIsLoadingMore1(false); + } + }, [comparisonData, isLoadingMore1, hasMoreData1, currentPage1]); + + // 加载更多素材 - 右侧 + const loadMoreMaterials2 = useCallback(async () => { + if (!comparisonData || isLoadingMore2 || !hasMoreData2) return; + + setIsLoadingMore2(true); + try { + const nextPage = currentPage2 + 1; + const response = await OutfitFavoriteService.searchMaterialsByFavorite( + comparisonData.favorite_2.id, + nextPage, + 9 + ); + + setComparisonData(prev => prev ? { + ...prev, + materials_2: { + ...response, + results: [...prev.materials_2.results, ...response.results] + } + } : null); + + setCurrentPage2(nextPage); + setHasMoreData2(nextPage < response.total_pages); + } catch (error) { + console.error('加载更多素材失败 (右侧):', error); + } finally { + setIsLoadingMore2(false); + } + }, [comparisonData, isLoadingMore2, hasMoreData2, currentPage2]); + + // 刷新素材 - 左侧 + const refreshMaterials1 = useCallback(async () => { + if (!comparisonData || isRefreshing1) return; + + setIsRefreshing1(true); + try { + const response = await OutfitFavoriteService.searchMaterialsByFavorite( + comparisonData.favorite_1.id, + 1, + 9 + ); + + setComparisonData(prev => prev ? { + ...prev, + materials_1: response + } : null); + + setCurrentPage1(1); + setHasMoreData1(1 < response.total_pages); + } catch (error) { + console.error('刷新素材失败 (左侧):', error); + } finally { + setIsRefreshing1(false); + } + }, [comparisonData, isRefreshing1]); + + // 刷新素材 - 右侧 + const refreshMaterials2 = useCallback(async () => { + if (!comparisonData || isRefreshing2) return; + + setIsRefreshing2(true); + try { + const response = await OutfitFavoriteService.searchMaterialsByFavorite( + comparisonData.favorite_2.id, + 1, + 9 + ); + + setComparisonData(prev => prev ? { + ...prev, + materials_2: response + } : null); + + setCurrentPage2(1); + setHasMoreData2(1 < response.total_pages); + } catch (error) { + console.error('刷新素材失败 (右侧):', error); + } finally { + setIsRefreshing2(false); + } + }, [comparisonData, isRefreshing2]); + + // 滚动监听 - 左侧 + useEffect(() => { + const container = scrollContainer1Ref.current; + if (!container) return; + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = container; + const isNearBottom = scrollTop + clientHeight >= scrollHeight - 100; + + if (isNearBottom && hasMoreData1 && !isLoadingMore1 && !isLoading) { + loadMoreMaterials1(); + } + }; + + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + }, [hasMoreData1, isLoadingMore1, isLoading, loadMoreMaterials1]); + + // 滚动监听 - 右侧 + useEffect(() => { + const container = scrollContainer2Ref.current; + if (!container) return; + + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = container; + const isNearBottom = scrollTop + clientHeight >= scrollHeight - 100; + + if (isNearBottom && hasMoreData2 && !isLoadingMore2 && !isLoading) { + loadMoreMaterials2(); + } + }; + + container.addEventListener('scroll', handleScroll); + return () => container.removeEventListener('scroll', handleScroll); + }, [hasMoreData2, isLoadingMore2, isLoading, loadMoreMaterials2]); + + // 将MaterialSearchResult转换为GroundingSource的适配器函数 + const convertToGroundingSource = useCallback((material: any): GroundingSource => { + return { + uri: material.image_url, + title: material.style_description || '素材图片', + content: { + text: material.style_description || '', + id: material.id, + material_type: material.material_type, + relevance_score: material.relevance_score, + environment_tags: material.environment_tags || [], + style_tags: material.style_tags || [], + occasion_tags: material.occasion_tags || [], + color_info: material.color_info, + // 为ImageCard提供必要的显示信息 + description: material.style_description, + models: [], + categories: material.style_tags || [], + environmentTags: material.environment_tags || [], + releaseDate: new Date().toISOString() + } + }; + }, []); + + // 下载素材到本地 + const downloadMaterial = useCallback(async (source: GroundingSource, materialId: string) => { + try { + if (!source.uri) { + console.error('图片URL不存在'); + return; + } + + // 获取文件扩展名 + const url = new URL(source.uri); + const pathname = url.pathname; + const extension = pathname.split('.').pop() || 'jpg'; + + // 打开保存对话框 + const filePath = await save({ + defaultPath: `material_${materialId}.${extension}`, + filters: [ + { + name: '图片文件', + extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'] + } + ] + }); + + if (!filePath) { + // 用户取消了保存 + return; + } + + // 使用Tauri命令下载文件 + await invoke('download_image_from_uri', { + uri: source.uri, + filePath: filePath + }); + + console.log('文件下载成功:', filePath); + } catch (error) { + console.error('下载文件失败:', error); + // 可以添加错误提示 + } + }, []); + // 初始加载 useEffect(() => { performComparison(); @@ -135,7 +380,7 @@ export const OutfitComparisonView: React.FC = ({ ); }; - // 渲染素材网格 + // 渲染素材网格 - 使用ImageCard组件 const renderMaterialGrid = (materials: any[], _side: 'left' | 'right') => { if (materials.length === 0) { return ( @@ -149,36 +394,27 @@ export const OutfitComparisonView: React.FC = ({ } return ( -
- {materials.map((material, index) => ( -
- Material { - e.currentTarget.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik0xMDAgNzBMMTMwIDEwMEgxMTBWMTMwSDkwVjEwMEg3MEwxMDAgNzBaIiBmaWxsPSIjOUI5QkEwIi8+Cjx0ZXh0IHg9IjEwMCIgeT0iMTUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjOUI5QkEwIiBmb250LXNpemU9IjEyIj7lm77niYfliKDpmaTlpLHotKU8L3RleHQ+Cjwvc3ZnPg=='; +
+ {materials.map((material, index) => { + const groundingSource = convertToGroundingSource(material); + return ( + { + // 可以添加查看大图的逻辑 + if (source.uri) { + window.open(source.uri, '_blank', 'noopener,noreferrer'); + } + }} + onDownload={(source) => { + downloadMaterial(source, material.id || `${index}`); }} /> -
-

- {material.style_description} -

-
- {material.environment_tags?.slice(0, 2).map((tag: string, i: number) => ( - - {tag} - - ))} -
- {material.relevance_score && ( -
- 相关度: {(material.relevance_score * 100).toFixed(1)}% -
- )} -
-
- ))} + ); + })}
); }; @@ -249,12 +485,51 @@ export const OutfitComparisonView: React.FC = ({

检索结果

- - {comparisonData.materials_1.total_size} 个结果 - +
+ {/* 刷新按钮 */} + + + + 已显示 {comparisonData.materials_1.results.length} / {comparisonData.materials_1.total_size} 个结果 + +
-
+
{renderMaterialGrid(comparisonData.materials_1.results, 'left')} + + {/* 加载更多指示器 */} + {isLoadingMore1 && ( +
+ + 加载更多... +
+ )} + + {/* 没有更多数据提示 */} + {!hasMoreData1 && comparisonData.materials_1.results.length > 0 && ( +
+ 已显示全部结果 +
+ )} + + {/* 手动加载更多按钮(备用) */} + {hasMoreData1 && !isLoadingMore1 && comparisonData.materials_1.results.length > 0 && ( +
+ +
+ )}
@@ -268,12 +543,51 @@ export const OutfitComparisonView: React.FC = ({

检索结果

- - {comparisonData.materials_2.total_size} 个结果 - +
+ {/* 刷新按钮 */} + + + + 已显示 {comparisonData.materials_2.results.length} / {comparisonData.materials_2.total_size} 个结果 + +
-
+
{renderMaterialGrid(comparisonData.materials_2.results, 'right')} + + {/* 加载更多指示器 */} + {isLoadingMore2 && ( +
+ + 加载更多... +
+ )} + + {/* 没有更多数据提示 */} + {!hasMoreData2 && comparisonData.materials_2.results.length > 0 && ( +
+ 已显示全部结果 +
+ )} + + {/* 手动加载更多按钮(备用) */} + {hasMoreData2 && !isLoadingMore2 && comparisonData.materials_2.results.length > 0 && ( +
+ +
+ )}