feat: 实现项目详情页面筛选功能优化 v0.1.25

- 为片段管理添加使用状态筛选条件(全部/已使用/未使用)
- 为素材管理添加AI分类筛选条件(基于实际分类记录)
- 为素材管理添加模特筛选条件(全部/未指定/具体模特)
- 为素材管理添加使用状态筛选条件(全部/已使用/未使用)
- 优化UI/UX设计,添加动画效果和视觉一致性
- 实现基于视频分类记录的真实数据筛选逻辑
- 添加筛选条件显示和清除功能
- 遵循promptx/frontend-developer设计标准
This commit is contained in:
imeepos
2025-07-16 18:25:37 +08:00
parent 49b9d46a13
commit 08fa4eda61
4 changed files with 414 additions and 24 deletions

View File

@@ -8,7 +8,8 @@ import {
Eye, Eye,
Edit, Edit,
Trash2, Trash2,
FolderOpen FolderOpen,
CheckCircle
} from 'lucide-react'; } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { SearchInput } from './InteractiveInput'; import { SearchInput } from './InteractiveInput';
@@ -28,6 +29,9 @@ interface SegmentWithDetails {
duration: number; duration: number;
file_path: string; file_path: string;
thumbnail_path?: string; thumbnail_path?: string;
usage_count: number;
is_used: boolean;
last_used_at?: string;
}; };
material_name: string; material_name: string;
material_type: string; material_type: string;
@@ -198,6 +202,7 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [selectedClassification, setSelectedClassification] = useState<string>('全部'); const [selectedClassification, setSelectedClassification] = useState<string>('全部');
const [selectedModel, setSelectedModel] = useState<string>('全部'); const [selectedModel, setSelectedModel] = useState<string>('全部');
const [selectedUsageStatus, setSelectedUsageStatus] = useState<string>('全部');
const [thumbnailCache, setThumbnailCache] = useState<Map<string, string>>(new Map()); const [thumbnailCache, setThumbnailCache] = useState<Map<string, string>>(new Map());
// 加载数据 // 加载数据
@@ -275,6 +280,47 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
return options; return options;
}, [segmentView]); }, [segmentView]);
// 获取使用状态选项
const usageStatusOptions = useMemo(() => {
if (!segmentView) return [
{ label: '全部', value: '全部', count: 0 },
{ label: '已使用', value: '已使用', count: 0 },
{ label: '未使用', value: '未使用', count: 0 }
];
// 统计使用状态
let usedCount = 0;
let unusedCount = 0;
segmentView.by_classification.forEach(group => {
group.segments.forEach(segment => {
if (segment.segment.is_used) {
usedCount++;
} else {
unusedCount++;
}
});
});
return [
{
label: '全部',
value: '全部',
count: segmentView.stats.total_segments
},
{
label: '已使用',
value: '已使用',
count: usedCount
},
{
label: '未使用',
value: '未使用',
count: unusedCount
}
];
}, [segmentView]);
// 获取过滤后的片段 // 获取过滤后的片段
const filteredSegments = useMemo(() => { const filteredSegments = useMemo(() => {
if (!segmentView) return []; if (!segmentView) return [];
@@ -299,6 +345,18 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
); );
} }
// 应用使用状态过滤
if (selectedUsageStatus !== '全部') {
segments = segments.filter(segment => {
if (selectedUsageStatus === '已使用') {
return segment.segment.is_used;
} else if (selectedUsageStatus === '未使用') {
return !segment.segment.is_used;
}
return true;
});
}
// 应用搜索过滤 // 应用搜索过滤
if (searchTerm.trim()) { if (searchTerm.trim()) {
const searchLower = searchTerm.toLowerCase(); const searchLower = searchTerm.toLowerCase();
@@ -310,7 +368,7 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
} }
return segments; return segments;
}, [segmentView, selectedClassification, selectedModel, searchTerm]); }, [segmentView, selectedClassification, selectedModel, selectedUsageStatus, searchTerm]);
// 渲染片段卡片 // 渲染片段卡片
const renderSegmentCard = (segment: SegmentWithDetails) => { const renderSegmentCard = (segment: SegmentWithDetails) => {
@@ -396,6 +454,16 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
: {Math.round(segment.classification.confidence * 100)}% : {Math.round(segment.classification.confidence * 100)}%
</span> </span>
)} )}
{/* 使用状态标识 */}
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
segment.segment.is_used
? 'bg-purple-100 text-purple-800'
: 'bg-gray-100 text-gray-600'
}`}>
<CheckCircle size={12} className="mr-1" />
{segment.segment.is_used ? `已使用 (${segment.segment.usage_count}次)` : '未使用'}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -462,9 +530,9 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
</div> </div>
{/* 筛选条件 */} {/* 筛选条件 */}
<div className="space-y-4"> <div className="space-y-4 animate-fadeIn">
{/* AI分类筛选 - 单行显示 */} {/* AI分类筛选 - 单行显示 */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4 p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<Tag size={16} className="text-gray-600" /> <Tag size={16} className="text-gray-600" />
<span className="text-sm font-medium text-gray-700">AI分类</span> <span className="text-sm font-medium text-gray-700">AI分类</span>
@@ -474,13 +542,13 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
<button <button
key={option.value} key={option.value}
onClick={() => setSelectedClassification(option.value)} onClick={() => setSelectedClassification(option.value)}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${selectedClassification === option.value className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200 hover:scale-105 ${selectedClassification === option.value
? 'bg-blue-100 text-blue-800 border border-blue-200' ? 'bg-blue-100 text-blue-800 border border-blue-200 shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200'
}`} }`}
> >
<span>{option.label}</span> <span>{option.label}</span>
<span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs"> <span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs shadow-sm">
{option.count} {option.count}
</span> </span>
</button> </button>
@@ -489,7 +557,7 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
</div> </div>
{/* 模特筛选 - 单行显示 */} {/* 模特筛选 - 单行显示 */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4 p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<Users size={16} className="text-gray-600" /> <Users size={16} className="text-gray-600" />
<span className="text-sm font-medium text-gray-700"></span> <span className="text-sm font-medium text-gray-700"></span>
@@ -499,13 +567,38 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
<button <button
key={option.value} key={option.value}
onClick={() => setSelectedModel(option.value)} onClick={() => setSelectedModel(option.value)}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${selectedModel === option.value className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200 hover:scale-105 ${selectedModel === option.value
? 'bg-green-100 text-green-800 border border-green-200' ? 'bg-green-100 text-green-800 border border-green-200 shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200' : 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200'
}`} }`}
> >
<span>{option.label}</span> <span>{option.label}</span>
<span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs"> <span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs shadow-sm">
{option.count}
</span>
</button>
))}
</div>
</div>
{/* 使用状态筛选 - 单行显示 */}
<div className="flex items-center gap-4 p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center gap-2 flex-shrink-0">
<CheckCircle size={16} className="text-gray-600" />
<span className="text-sm font-medium text-gray-700">使</span>
</div>
<div className="flex flex-wrap gap-2">
{usageStatusOptions.map(option => (
<button
key={option.value}
onClick={() => setSelectedUsageStatus(option.value)}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200 hover:scale-105 ${selectedUsageStatus === option.value
? 'bg-purple-100 text-purple-800 border border-purple-200 shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200'
}`}
>
<span>{option.label}</span>
<span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs shadow-sm">
{option.count} {option.count}
</span> </span>
</button> </button>
@@ -514,16 +607,16 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
</div> </div>
{/* 当前筛选条件显示 */} {/* 当前筛选条件显示 */}
{(selectedClassification !== '全部' || selectedModel !== '全部') && ( {(selectedClassification !== '全部' || selectedModel !== '全部' || selectedUsageStatus !== '全部') && (
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded-lg"> <div className="flex items-center gap-2 p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border border-blue-200 shadow-sm animate-slideIn">
<Filter size={16} className="text-gray-500" /> <Filter size={16} className="text-blue-600" />
<span className="text-sm text-gray-600"></span> <span className="text-sm font-medium text-gray-700"></span>
{selectedClassification !== '全部' && ( {selectedClassification !== '全部' && (
<span className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full"> <span className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
AI分类: {selectedClassification} AI分类: {selectedClassification}
</span> </span>
)} )}
{selectedClassification !== '全部' && selectedModel !== '全部' && ( {(selectedClassification !== '全部' && selectedModel !== '全部') && (
<span className="text-xs text-gray-500">AND</span> <span className="text-xs text-gray-500">AND</span>
)} )}
{selectedModel !== '全部' && ( {selectedModel !== '全部' && (
@@ -531,13 +624,23 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
: {selectedModel} : {selectedModel}
</span> </span>
)} )}
{((selectedClassification !== '全部' || selectedModel !== '全部') && selectedUsageStatus !== '全部') && (
<span className="text-xs text-gray-500">AND</span>
)}
{selectedUsageStatus !== '全部' && (
<span className="inline-flex items-center px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded-full">
使: {selectedUsageStatus}
</span>
)}
<button <button
onClick={() => { onClick={() => {
setSelectedClassification('全部'); setSelectedClassification('全部');
setSelectedModel('全部'); setSelectedModel('全部');
setSelectedUsageStatus('全部');
}} }}
className="ml-auto text-xs text-gray-500 hover:text-gray-700" className="ml-auto inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-300 rounded-full hover:bg-gray-50 hover:text-gray-700 transition-all duration-200 hover:scale-105 shadow-sm"
> >
<Filter size={12} className="mr-1" />
</button> </button>
</div> </div>

View File

@@ -27,7 +27,7 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
projectId, projectId,
templateId, templateId,
bindingId, bindingId,
showStats = true, showStats = false,
onResultSelect, onResultSelect,
}) => { }) => {
const [results, setResults] = useState<TemplateMatchingResult[]>([]); const [results, setResults] = useState<TemplateMatchingResult[]>([]);

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, FolderOpen, Upload, FileVideo, FileAudio, FileImage, HardDrive, Brain, Loader2, Link, Layers, Calendar, MapPin } from 'lucide-react'; import { ArrowLeft, FolderOpen, Upload, FileVideo, FileAudio, FileImage, HardDrive, Brain, Loader2, Link, Layers, Calendar, MapPin, Users, CheckCircle, Filter } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useProjectStore } from '../store/projectStore'; import { useProjectStore } from '../store/projectStore';
import { useMaterialStore } from '../store/materialStore'; import { useMaterialStore } from '../store/materialStore';
@@ -129,6 +129,12 @@ export const ProjectDetails: React.FC = () => {
const [segmentStats, setSegmentStats] = useState<any>(null); const [segmentStats, setSegmentStats] = useState<any>(null);
const [currentMatchingBinding, setCurrentMatchingBinding] = useState<ProjectTemplateBindingDetail | null>(null); const [currentMatchingBinding, setCurrentMatchingBinding] = useState<ProjectTemplateBindingDetail | null>(null);
// 素材筛选状态
const [materialClassificationFilter, setMaterialClassificationFilter] = useState<string>('全部');
const [materialModelFilter, setMaterialModelFilter] = useState<string>('全部');
const [materialUsageFilter, setMaterialUsageFilter] = useState<string>('全部');
const [materialClassificationRecords, setMaterialClassificationRecords] = useState<{[materialId: string]: any[]}>({});
// 加载片段统计数据 // 加载片段统计数据
const loadSegmentStats = useCallback(async (projectId: string) => { const loadSegmentStats = useCallback(async (projectId: string) => {
try { try {
@@ -149,6 +155,28 @@ export const ProjectDetails: React.FC = () => {
} }
}, []); }, []);
// 加载项目分类统计信息
const loadProjectClassificationStats = useCallback(async (projectId: string) => {
try {
// 获取每个素材的分类记录
const classificationRecords: {[materialId: string]: any[]} = {};
for (const material of materials) {
try {
const records = await invoke('get_material_classification_records', { materialId: material.id }) as any[];
classificationRecords[material.id] = records;
} catch (error) {
console.warn(`Failed to load classification records for material ${material.id}:`, error);
classificationRecords[material.id] = [];
}
}
setMaterialClassificationRecords(classificationRecords);
} catch (error) {
console.error('Failed to load project classification stats:', error);
setMaterialClassificationRecords({});
}
}, [materials]);
// 加载项目详情 // 加载项目详情
useEffect(() => { useEffect(() => {
if (!projects.length) { if (!projects.length) {
@@ -172,9 +200,18 @@ export const ProjectDetails: React.FC = () => {
loadSegmentStats(foundProject.id); loadSegmentStats(foundProject.id);
// 加载素材使用状态概览 // 加载素材使用状态概览
loadUsageOverview(foundProject.id); loadUsageOverview(foundProject.id);
// 加载项目分类统计信息
loadProjectClassificationStats(foundProject.id);
} }
} }
}, [id, projects, loadMaterials, loadMaterialStats, bindingActions.fetchTemplatesByProject, loadSegmentStats]); }, [id, projects, loadMaterials, loadMaterialStats, bindingActions.fetchTemplatesByProject, loadSegmentStats, loadUsageOverview, loadProjectClassificationStats]);
// 当素材列表变化时,重新加载分类统计信息
useEffect(() => {
if (project && materials.length > 0) {
loadProjectClassificationStats(project.id);
}
}, [materials.length, project, loadProjectClassificationStats]);
// 加载模板列表 // 加载模板列表
useEffect(() => { useEffect(() => {
@@ -519,6 +556,114 @@ export const ProjectDetails: React.FC = () => {
} }
}; };
// 素材筛选选项
const materialClassificationOptions = useMemo(() => {
const options = [{ label: '全部', value: '全部', count: materials.length }];
// 统计分类信息
const categoryCount: {[category: string]: number} = {};
// 遍历所有素材的分类记录
Object.values(materialClassificationRecords).forEach(records => {
records.forEach(record => {
if (record.category) {
categoryCount[record.category] = (categoryCount[record.category] || 0) + 1;
}
});
});
// 添加分类选项
Object.entries(categoryCount).forEach(([category, count]) => {
options.push({
label: category,
value: category,
count
});
});
return options;
}, [materials.length, materialClassificationRecords]);
const materialModelOptions = useMemo(() => {
const options = [{ label: '全部', value: '全部', count: materials.length }];
// 统计模特信息
const modelCounts: {[key: string]: number} = {};
materials.forEach(material => {
if (material.model_id) {
// 这里需要根据model_id获取模特名称暂时使用model_id
const modelKey = material.model_id;
modelCounts[modelKey] = (modelCounts[modelKey] || 0) + 1;
} else {
modelCounts['未指定'] = (modelCounts['未指定'] || 0) + 1;
}
});
Object.entries(modelCounts).forEach(([modelKey, count]) => {
options.push({
label: modelKey === '未指定' ? '未指定' : `模特-${modelKey}`,
value: modelKey,
count
});
});
return options;
}, [materials]);
const materialUsageOptions = useMemo(() => {
// 计算使用状态统计
let usedCount = 0;
let unusedCount = 0;
materials.forEach(material => {
const hasUsedSegments = material.segments.some(segment => segment.is_used);
if (hasUsedSegments) {
usedCount++;
} else {
unusedCount++;
}
});
return [
{ label: '全部', value: '全部', count: materials.length },
{ label: '已使用', value: '已使用', count: usedCount },
{ label: '未使用', value: '未使用', count: unusedCount }
];
}, [materials]);
// 过滤后的素材列表
const filteredMaterials = useMemo(() => {
return materials.filter(material => {
// AI分类过滤
if (materialClassificationFilter !== '全部') {
const materialRecords = materialClassificationRecords[material.id] || [];
const hasMatchingClassification = materialRecords.some(record =>
record.category === materialClassificationFilter
);
if (!hasMatchingClassification) return false;
}
// 模特过滤
if (materialModelFilter !== '全部') {
if (materialModelFilter === '未指定') {
if (material.model_id) return false;
} else {
// 检查model_id是否匹配
if (material.model_id !== materialModelFilter) return false;
}
}
// 使用状态过滤
if (materialUsageFilter !== '全部') {
const hasUsedSegments = material.segments.some(segment => segment.is_used);
if (materialUsageFilter === '已使用' && !hasUsedSegments) return false;
if (materialUsageFilter === '未使用' && hasUsedSegments) return false;
}
return true;
});
}, [materials, materialClassificationFilter, materialModelFilter, materialUsageFilter, materialClassificationRecords]);
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center min-h-[400px]"> <div className="flex items-center justify-center min-h-[400px]">
@@ -856,6 +1001,124 @@ export const ProjectDetails: React.FC = () => {
{/* 素材管理选项卡 */} {/* 素材管理选项卡 */}
{activeTab === 'materials' && ( {activeTab === 'materials' && (
<div className="p-4 md:p-6 space-y-6"> <div className="p-4 md:p-6 space-y-6">
{/* 素材筛选条件 */}
<div className="space-y-4 animate-fadeIn">
{/* AI分类筛选 */}
<div className="flex items-center gap-4 p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center gap-2 flex-shrink-0">
<Brain size={16} className="text-gray-600" />
<span className="text-sm font-medium text-gray-700">AI分类</span>
</div>
<div className="flex flex-wrap gap-2">
{materialClassificationOptions.map(option => (
<button
key={option.value}
onClick={() => setMaterialClassificationFilter(option.value)}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200 hover:scale-105 ${materialClassificationFilter === option.value
? 'bg-blue-100 text-blue-800 border border-blue-200 shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200'
}`}
>
<span>{option.label}</span>
<span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs shadow-sm">
{option.count}
</span>
</button>
))}
</div>
</div>
{/* 模特筛选 */}
<div className="flex items-center gap-4 p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center gap-2 flex-shrink-0">
<Users size={16} className="text-gray-600" />
<span className="text-sm font-medium text-gray-700"></span>
</div>
<div className="flex flex-wrap gap-2">
{materialModelOptions.map(option => (
<button
key={option.value}
onClick={() => setMaterialModelFilter(option.value)}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200 hover:scale-105 ${materialModelFilter === option.value
? 'bg-green-100 text-green-800 border border-green-200 shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200'
}`}
>
<span>{option.label}</span>
<span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs shadow-sm">
{option.count}
</span>
</button>
))}
</div>
</div>
{/* 使用状态筛选 */}
<div className="flex items-center gap-4 p-4 bg-white rounded-lg border border-gray-200 shadow-sm">
<div className="flex items-center gap-2 flex-shrink-0">
<CheckCircle size={16} className="text-gray-600" />
<span className="text-sm font-medium text-gray-700">使</span>
</div>
<div className="flex flex-wrap gap-2">
{materialUsageOptions.map(option => (
<button
key={option.value}
onClick={() => setMaterialUsageFilter(option.value)}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium transition-all duration-200 hover:scale-105 ${materialUsageFilter === option.value
? 'bg-purple-100 text-purple-800 border border-purple-200 shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border border-gray-200'
}`}
>
<span>{option.label}</span>
<span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs shadow-sm">
{option.count}
</span>
</button>
))}
</div>
</div>
{/* 当前筛选条件显示 */}
{(materialClassificationFilter !== '全部' || materialModelFilter !== '全部' || materialUsageFilter !== '全部') && (
<div className="flex items-center gap-2 p-4 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg border border-blue-200 shadow-sm animate-slideIn">
<Filter size={16} className="text-blue-600" />
<span className="text-sm font-medium text-gray-700"></span>
{materialClassificationFilter !== '全部' && (
<span className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
AI分类: {materialClassificationFilter}
</span>
)}
{((materialClassificationFilter !== '全部') && (materialModelFilter !== '全部')) && (
<span className="text-xs text-gray-500">AND</span>
)}
{materialModelFilter !== '全部' && (
<span className="inline-flex items-center px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
: {materialModelFilter}
</span>
)}
{((materialClassificationFilter !== '全部' || materialModelFilter !== '全部') && materialUsageFilter !== '全部') && (
<span className="text-xs text-gray-500">AND</span>
)}
{materialUsageFilter !== '全部' && (
<span className="inline-flex items-center px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded-full">
使: {materialUsageFilter}
</span>
)}
<button
onClick={() => {
setMaterialClassificationFilter('全部');
setMaterialModelFilter('全部');
setMaterialUsageFilter('全部');
}}
className="ml-auto inline-flex items-center px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-300 rounded-full hover:bg-gray-50 hover:text-gray-700 transition-all duration-200 hover:scale-105 shadow-sm"
>
<Filter size={12} className="mr-1" />
</button>
</div>
)}
</div>
{/* 素材列表 */} {/* 素材列表 */}
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@@ -873,9 +1136,9 @@ export const ProjectDetails: React.FC = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 md:gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 md:gap-4">
<MaterialCardSkeleton count={8} /> <MaterialCardSkeleton count={8} />
</div> </div>
) : materials.length > 0 ? ( ) : filteredMaterials.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 md:gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 md:gap-4">
{materials.map((material) => ( {filteredMaterials.map((material) => (
<MaterialCard <MaterialCard
key={material.id} key={material.id}
material={material} material={material}
@@ -886,6 +1149,27 @@ export const ProjectDetails: React.FC = () => {
/> />
))} ))}
</div> </div>
) : materials.length > 0 ? (
<div className="text-center py-16">
<div className="w-20 h-20 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
<Filter className="w-10 h-10 text-gray-400" />
</div>
<h4 className="text-xl font-medium text-gray-900 mb-2"></h4>
<p className="text-gray-500 mb-6 max-w-sm mx-auto">
</p>
<button
onClick={() => {
setMaterialClassificationFilter('全部');
setMaterialModelFilter('全部');
setMaterialUsageFilter('全部');
}}
className="inline-flex items-center px-6 py-3 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
<Filter className="w-5 h-5 mr-2" />
</button>
</div>
) : ( ) : (
<div className="text-center py-16"> <div className="text-center py-16">
<div className="w-20 h-20 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center"> <div className="w-20 h-20 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
@@ -972,7 +1256,7 @@ export const ProjectDetails: React.FC = () => {
</div> </div>
<TemplateMatchingResultManager <TemplateMatchingResultManager
projectId={project.id} projectId={project.id}
showStats={true} showStats={false}
/> />
</div> </div>
)} )}

View File

@@ -70,6 +70,9 @@ export interface MaterialSegment {
file_path: string; file_path: string;
file_size: number; file_size: number;
thumbnail_path?: string; // 缩略图路径 thumbnail_path?: string; // 缩略图路径
usage_count: number; // 使用次数
is_used: boolean; // 是否已使用
last_used_at?: string; // 最后使用时间
created_at: string; created_at: string;
} }