mixvideo-v2/apps/desktop/src/components/material/MaterialSearchResults.tsx

267 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useState } from 'react';
import {
Grid,
List,
Loader2,
AlertCircle,
Package,
} from 'lucide-react';
import { MaterialSearchResultsProps } from '../../types/outfitRecommendation';
import { MaterialCard, MaterialSearchPagination } from './index';
import { EmptyState } from '../EmptyState';
import MaterialSearchService from '../../services/materialSearchService';
/**
* 素材检索结果列表组件
* 遵循设计系统规范,提供统一的结果展示界面
*/
const MaterialSearchResults: React.FC<MaterialSearchResultsProps> = ({
results,
totalSize,
currentPage,
pageSize,
isLoading = false,
error,
onPageChange,
onMaterialSelect,
className = '',
}) => {
// 视图模式状态
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
// 计算分页信息
const totalPages = MaterialSearchService.getTotalPages(totalSize, pageSize);
const pageInfo = MaterialSearchService.getPageRangeInfo(currentPage, pageSize, totalSize);
// 处理页面变化
const handlePageChange = useCallback((page: number) => {
if (page >= 1 && page <= totalPages && !isLoading) {
onPageChange(page);
}
}, [totalPages, onPageChange, isLoading]);
// 处理素材选择
const handleMaterialSelect = useCallback((material: any) => {
if (onMaterialSelect) {
onMaterialSelect(material);
}
}, [onMaterialSelect]);
// 加载状态
if (isLoading && results.length === 0) {
return (
<div className={`space-y-6 ${className}`}>
{/* 加载标题 */}
<div className="flex items-center justify-center gap-3 p-6">
<Loader2 className="w-6 h-6 text-primary-500 animate-spin" />
<div className="text-center">
<h3 className="text-lg font-semibold text-high-emphasis mb-1">
...
</h3>
<p className="text-sm text-medium-emphasis">
</p>
</div>
</div>
{/* 加载骨架屏 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map((index) => (
<div
key={index}
className="card p-4 animate-pulse"
>
<div className="space-y-4">
{/* 图片骨架 */}
<div className="aspect-square bg-gray-200 rounded-lg"></div>
{/* 标题骨架 */}
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
{/* 描述骨架 */}
<div className="space-y-2">
<div className="h-3 bg-gray-200 rounded w-full"></div>
<div className="h-3 bg-gray-200 rounded w-2/3"></div>
</div>
{/* 标签骨架 */}
<div className="flex gap-2">
<div className="h-6 bg-gray-200 rounded-full w-16"></div>
<div className="h-6 bg-gray-200 rounded-full w-12"></div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
// 错误状态
if (error) {
return (
<div className={`${className}`}>
<EmptyState
variant="error"
icon={<AlertCircle className="w-12 h-12 text-red-500" />}
title="检索失败"
description={error}
actionText="重新检索"
onAction={() => onPageChange(currentPage)}
illustration="error"
size="md"
className="py-12"
/>
</div>
);
}
// 空状态
if (!results || results.length === 0) {
return (
<div className={`${className}`}>
<EmptyState
variant="default"
icon={<Package className="w-12 h-12 text-gray-400" />}
title="暂无素材结果"
description="未找到符合条件的素材,请尝试调整检索条件"
actionText="重新检索"
onAction={() => onPageChange(1)}
illustration="folder"
size="md"
className="py-12"
showTips={true}
tips={[
'尝试使用更通用的关键词',
'调整颜色和风格过滤条件',
'检查网络连接是否正常',
]}
/>
</div>
);
}
// 正常状态 - 显示结果列表
return (
<div className={`space-y-6 max-h-full overflow-y-auto ${className}`}>
{/* 列表标题和统计信息 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="icon-container primary w-8 h-8">
<Grid className="w-4 h-4" />
</div>
<div>
<h3 className="text-lg font-semibold text-high-emphasis">
</h3>
<p className="text-sm text-medium-emphasis">
{pageInfo.start}-{pageInfo.end} {totalSize}
</p>
</div>
</div>
{/* 视图切换 */}
<div className="flex items-center gap-2">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-lg transition-colors duration-200 ${
viewMode === 'grid'
? 'text-primary-600 bg-primary-50'
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-100'
}`}
title="网格视图"
>
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-lg transition-colors duration-200 ${
viewMode === 'list'
? 'text-primary-600 bg-primary-50'
: 'text-gray-400 hover:text-gray-600 hover:bg-gray-100'
}`}
title="列表视图"
>
<List className="w-4 h-4" />
</button>
</div>
</div>
{/* 素材卡片展示 */}
<div className={
viewMode === 'grid'
? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
: "space-y-4"
}>
{results.map((material, index) => (
<MaterialCard
key={material.id || index}
material={material}
onSelect={handleMaterialSelect}
showScore={true}
compact={viewMode === 'list'}
className={`animate-fade-in-up ${isLoading ? 'opacity-50 pointer-events-none' : ''} ${
viewMode === 'list' ? 'flex flex-row items-center gap-4 p-4' : ''
}`}
/>
))}
</div>
{/* 分页控制 */}
{totalPages > 1 && (
<MaterialSearchPagination
currentPage={currentPage}
totalPages={totalPages}
pageSize={pageSize}
totalSize={totalSize}
isLoading={isLoading}
onPageChange={handlePageChange}
className="mt-8"
/>
)}
{/* 底部提示 */}
<div className="card p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200">
<div className="flex items-start gap-3">
<div className="icon-container blue w-6 h-6 mt-0.5 shadow-sm">
<Package className="w-3 h-3" />
</div>
<div className="flex-1">
<h4 className="text-sm font-semibold text-blue-900 mb-2">💡 使</h4>
<ul className="text-xs text-blue-700 space-y-1.5">
<li className="flex items-center gap-2">
<div className="w-1 h-1 bg-blue-400 rounded-full"></div>
</li>
<li className="flex items-center gap-2">
<div className="w-1 h-1 bg-blue-400 rounded-full"></div>
使
</li>
<li className="flex items-center gap-2">
<div className="w-1 h-1 bg-blue-400 rounded-full"></div>
穿
</li>
<li className="flex items-center gap-2">
<div className="w-1 h-1 bg-blue-400 rounded-full"></div>
</li>
</ul>
</div>
</div>
</div>
{/* 加载遮罩 */}
{isLoading && results.length > 0 && (
<div className="absolute inset-0 bg-white/50 backdrop-blur-sm flex items-center justify-center">
<div className="flex items-center gap-3 bg-white rounded-lg shadow-lg p-4">
<Loader2 className="w-5 h-5 text-primary-500 animate-spin" />
<span className="text-sm font-medium text-gray-700">...</span>
</div>
</div>
)}
</div>
);
};
export default MaterialSearchResults;