feat: 修复素材查看功能并添加详情模态框

修复问题:
- 修复点击查看/查看详情按钮没有反应的问题
- 完善素材选择回调处理逻辑

 新增功能:
- 创建MaterialDetailModal组件,提供完整的素材详情展示
- 支持素材图片查看、下载、分享、收藏功能
- 显示完整的素材信息:基本信息、环境标签、产品详情
- 美观的模态框设计,包含操作按钮和详细信息

 UI优化:
- 高质量的素材详情界面设计
- 支持图片加载状态和错误处理
- 评分标识和AI推荐标识
- 颜色信息可视化显示
- 响应式布局适配

 交互改进:
- 点击查看按钮现在会打开详情模态框
- 支持在新窗口打开原图
- 支持下载和分享功能
- 收藏状态管理

现在用户可以正常查看素材详情了!
This commit is contained in:
imeepos 2025-07-25 15:53:19 +08:00
parent 99763ebefb
commit 8a74a14970
6 changed files with 354 additions and 43 deletions

View File

@ -0,0 +1,332 @@
import React, { useState, useCallback } from 'react';
import {
X,
Star,
Tag,
Palette,
ExternalLink,
Download,
Heart,
Share2,
Calendar,
Package,
Eye,
Sparkles,
} from 'lucide-react';
interface MaterialDetailModalProps {
material: any;
isVisible: boolean;
onClose: () => void;
className?: string;
}
/**
*
*
*/
export const MaterialDetailModal: React.FC<MaterialDetailModalProps> = ({
material,
isVisible,
onClose,
className = '',
}) => {
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const [isFavorited, setIsFavorited] = useState(false);
// 处理图片加载
const handleImageLoad = useCallback(() => {
setImageLoaded(true);
setImageError(false);
}, []);
const handleImageError = useCallback(() => {
setImageLoaded(false);
setImageError(true);
}, []);
// 处理收藏
const handleToggleFavorite = useCallback(() => {
setIsFavorited(!isFavorited);
// 这里可以添加实际的收藏逻辑
}, [isFavorited]);
// 处理分享
const handleShare = useCallback(() => {
if (navigator.share && material.image_url) {
navigator.share({
title: material.style_description || '时尚素材',
url: material.image_url,
});
} else {
// 复制链接到剪贴板
navigator.clipboard.writeText(material.image_url || '');
alert('链接已复制到剪贴板');
}
}, [material]);
// 处理下载
const handleDownload = useCallback(() => {
if (material.image_url) {
const link = document.createElement('a');
link.href = material.image_url;
link.download = `material-${material.id || Date.now()}.jpg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}, [material]);
// 处理在新窗口打开
const handleOpenInNewWindow = useCallback(() => {
if (material.image_url) {
window.open(material.image_url, '_blank');
}
}, [material]);
if (!isVisible) return null;
const primaryProduct = material.products?.[0];
return (
<div className={`fixed inset-0 z-50 bg-black/50 backdrop-blur-sm animate-fade-in ${className}`}>
<div className="flex items-center justify-center min-h-screen p-4">
<div className="bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-hidden animate-fade-in-up">
{/* 头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 bg-gradient-to-r from-primary-50 to-blue-50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-primary-500 to-blue-500 rounded-xl flex items-center justify-center shadow-lg">
<Package className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-xl font-bold text-high-emphasis">
</h2>
<p className="text-sm text-medium-emphasis">
{material.style_description || '时尚素材详细信息'}
</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors duration-200"
aria-label="关闭"
>
<X className="w-6 h-6" />
</button>
</div>
{/* 主要内容 */}
<div className="flex-1 overflow-y-auto max-h-[calc(90vh-80px)]">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
{/* 左侧:图片展示 */}
<div className="space-y-4">
<div className="relative aspect-square rounded-xl overflow-hidden bg-gray-100 shadow-lg">
{!imageError ? (
<img
src={material.image_url}
alt={material.style_description}
className={`w-full h-full object-cover transition-all duration-300 ${
imageLoaded ? 'opacity-100' : 'opacity-0'
}`}
onLoad={handleImageLoad}
onError={handleImageError}
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gray-100">
<Package className="w-16 h-16 text-gray-400" />
</div>
)}
{/* 加载状态 */}
{!imageLoaded && !imageError && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-100">
<div className="w-8 h-8 border-4 border-primary-500 border-t-transparent rounded-full animate-spin"></div>
</div>
)}
{/* 评分标识 */}
<div className="absolute top-4 left-4 flex items-center gap-1 px-3 py-1 bg-yellow-100 text-yellow-700 rounded-full text-sm font-medium shadow-sm">
<Star className="w-4 h-4 fill-current" />
{material.relevance_score ? (material.relevance_score * 100).toFixed(1) : 'N/A'}%
</div>
{/* AI推荐标识 */}
<div className="absolute top-4 right-4 flex items-center gap-1 px-3 py-1 bg-gradient-to-r from-purple-100 to-pink-100 text-purple-700 rounded-full text-sm font-medium shadow-sm">
<Sparkles className="w-4 h-4" />
AI推荐
</div>
</div>
{/* 操作按钮 */}
<div className="flex items-center gap-2">
<button
onClick={handleToggleFavorite}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors duration-200 ${
isFavorited
? 'bg-red-50 text-red-600 hover:bg-red-100'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<Heart className={`w-4 h-4 ${isFavorited ? 'fill-current' : ''}`} />
{isFavorited ? '已收藏' : '收藏'}
</button>
<button
onClick={handleShare}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-600 hover:bg-gray-200 rounded-lg transition-colors duration-200"
>
<Share2 className="w-4 h-4" />
</button>
<button
onClick={handleDownload}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-600 hover:bg-gray-200 rounded-lg transition-colors duration-200"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={handleOpenInNewWindow}
className="flex items-center gap-2 px-4 py-2 bg-primary-500 text-white hover:bg-primary-600 rounded-lg transition-colors duration-200"
>
<ExternalLink className="w-4 h-4" />
</button>
</div>
</div>
{/* 右侧:详细信息 */}
<div className="space-y-6">
{/* 基本信息 */}
<div className="card p-4">
<h3 className="text-lg font-semibold text-high-emphasis mb-3 flex items-center gap-2">
<Tag className="w-5 h-5 text-primary-500" />
</h3>
<div className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-700"></label>
<p className="text-gray-900 mt-1">{material.style_description || '暂无描述'}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700"></label>
<p className="text-gray-900 mt-1">
{material.relevance_score ? `${(material.relevance_score * 100).toFixed(1)}%` : '暂无评分'}
</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700"></label>
<p className="text-gray-900 mt-1 flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-500" />
{new Date(material.created_at).toLocaleString()}
</p>
</div>
</div>
</div>
{/* 环境标签 */}
{material.environment_tags && material.environment_tags.length > 0 && (
<div className="card p-4">
<h3 className="text-lg font-semibold text-high-emphasis mb-3"></h3>
<div className="flex flex-wrap gap-2">
{material.environment_tags.map((tag: string, index: number) => (
<div
key={index}
className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm"
>
{tag}
</div>
))}
</div>
</div>
)}
{/* 产品信息 */}
{primaryProduct && (
<div className="card p-4">
<h3 className="text-lg font-semibold text-high-emphasis mb-3 flex items-center gap-2">
<Palette className="w-5 h-5 text-primary-500" />
</h3>
<div className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-700"></label>
<p className="text-gray-900 mt-1">{primaryProduct.category}</p>
</div>
<div>
<label className="text-sm font-medium text-gray-700"></label>
<p className="text-gray-900 mt-1">{primaryProduct.description}</p>
</div>
{primaryProduct.color_pattern && (
<div>
<label className="text-sm font-medium text-gray-700"></label>
<div className="flex items-center gap-2 mt-1">
<div
className="w-6 h-6 rounded-full border border-gray-300 shadow-sm"
style={{
backgroundColor: `hsl(${primaryProduct.color_pattern.hue * 360}, ${primaryProduct.color_pattern.saturation * 100}%, ${primaryProduct.color_pattern.value * 100}%)`
}}
/>
<span className="text-gray-900">
HSV({Math.round(primaryProduct.color_pattern.hue * 360)}, {Math.round(primaryProduct.color_pattern.saturation * 100)}%, {Math.round(primaryProduct.color_pattern.value * 100)}%)
</span>
</div>
</div>
)}
{primaryProduct.design_styles && primaryProduct.design_styles.length > 0 && (
<div>
<label className="text-sm font-medium text-gray-700"></label>
<div className="flex flex-wrap gap-2 mt-1">
{primaryProduct.design_styles.map((style: string, index: number) => (
<div
key={index}
className="px-2 py-1 bg-gray-100 text-gray-600 rounded text-sm"
>
{style}
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
{/* 其他产品 */}
{material.products && material.products.length > 1 && (
<div className="card p-4">
<h3 className="text-lg font-semibold text-high-emphasis mb-3">
({material.products.length - 1} )
</h3>
<div className="space-y-2">
{material.products.slice(1).map((product: any, index: number) => (
<div key={index} className="flex items-center gap-3 p-2 bg-gray-50 rounded">
<div className="text-sm font-medium text-gray-700">{product.category}</div>
<div className="text-sm text-gray-600">{product.description}</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default MaterialDetailModal;

View File

@ -7,6 +7,7 @@ export { default as MaterialSearchPanel } from './MaterialSearchPanel';
export { default as MaterialSearchResults } from './MaterialSearchResults';
export { default as MaterialCard } from './MaterialCard';
export { default as MaterialSearchPagination } from './MaterialSearchPagination';
export { MaterialDetailModal } from './MaterialDetailModal';
// 类型导出
export type {

View File

@ -21,14 +21,6 @@ const AdvancedFilterTool: React.FC = () => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-4">
<button
onClick={handleGoBack}
className="btn-icon btn-ghost hover-lift"
aria-label="返回工具列表"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div>
<h1 className="text-xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-600">

View File

@ -127,14 +127,6 @@ const ChatTestPage: React.FC = () => {
{/* 页面头部 */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 bg-white">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/tools')}
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-all duration-200"
title="返回工具列表"
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-blue-600 rounded-xl flex items-center justify-center shadow-lg">
<MessageCircle className="w-6 h-6 text-white" />

View File

@ -15,7 +15,7 @@ import OutfitRecommendationService from '../../services/outfitRecommendationServ
import { OutfitRecommendation, STYLE_OPTIONS, OCCASION_OPTIONS, SEASON_OPTIONS } from '../../types/outfitRecommendation';
import OutfitRecommendationList from '../../components/outfit/OutfitRecommendationList';
import { CustomSelect } from '../../components/CustomSelect';
import { MaterialSearchPanel } from '../../components/material';
import { MaterialSearchPanel, MaterialDetailModal } from '../../components/material';
/**
* AI穿搭方案推荐小工具
@ -41,10 +41,9 @@ const OutfitRecommendationTool: React.FC = () => {
const [showMaterialSearch, setShowMaterialSearch] = useState(false);
const [selectedRecommendation, setSelectedRecommendation] = useState<OutfitRecommendation | null>(null);
// 返回工具列表
const handleBackToTools = useCallback(() => {
navigate('/tools');
}, [navigate]);
// 素材详情状态
const [showMaterialDetail, setShowMaterialDetail] = useState(false);
const [selectedMaterial, setSelectedMaterial] = useState<any>(null);
// 生成穿搭方案
const handleGenerate = useCallback(async () => {
@ -154,7 +153,14 @@ const OutfitRecommendationTool: React.FC = () => {
// 处理素材选择
const handleMaterialSelect = useCallback((material: any) => {
console.log('选择素材:', material);
// 这里可以添加更多的处理逻辑,比如保存到收藏等
setSelectedMaterial(material);
setShowMaterialDetail(true);
}, []);
// 关闭素材详情
const handleCloseMaterialDetail = useCallback(() => {
setShowMaterialDetail(false);
setSelectedMaterial(null);
}, []);
return (
@ -164,14 +170,6 @@ const OutfitRecommendationTool: React.FC = () => {
<div className="mx-auto px-6 py-4">
<div className="page-header flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={handleBackToTools}
className="flex items-center gap-2 px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200"
>
<ArrowLeft className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</button>
<div className="h-6 w-px bg-gray-300"></div>
<div className="flex items-center gap-3">
@ -388,6 +386,15 @@ const OutfitRecommendationTool: React.FC = () => {
onMaterialSelect={handleMaterialSelect}
/>
)}
{/* 素材详情模态框 */}
{selectedMaterial && (
<MaterialDetailModal
material={selectedMaterial}
isVisible={showMaterialDetail}
onClose={handleCloseMaterialDetail}
/>
)}
</div>
);
};

View File

@ -182,11 +182,6 @@ const SimilaritySearchTool: React.FC = () => {
}
}, [selectedThreshold, searchConfig]); // 监听阈值和高级过滤器配置变化
// 返回工具列表
const handleBackToTools = useCallback(() => {
navigate('/tools');
}, [navigate]);
// 处理穿搭方案生成
const handleOutfitRecommendation = useCallback(async () => {
if (!query.trim()) {
@ -272,14 +267,6 @@ const SimilaritySearchTool: React.FC = () => {
<div className="mx-auto py-4 sm:py-6">
<div className="page-header flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={handleBackToTools}
className="flex items-center gap-2 py-2 text-gray-600 hover:text-primary-600 hover:bg-gray-50 rounded-lg transition-all duration-200"
>
<ArrowLeft className="w-4 h-4" />
<span className="text-sm font-medium"></span>
</button>
<div className="h-6 w-px bg-gray-200"></div>
<div className="flex items-center gap-3">