feat: 实现项目详情页面筛选功能优化 v0.1.25
- 为片段管理添加使用状态筛选条件(全部/已使用/未使用) - 为素材管理添加AI分类筛选条件(基于实际分类记录) - 为素材管理添加模特筛选条件(全部/未指定/具体模特) - 为素材管理添加使用状态筛选条件(全部/已使用/未使用) - 优化UI/UX设计,添加动画效果和视觉一致性 - 实现基于视频分类记录的真实数据筛选逻辑 - 添加筛选条件显示和清除功能 - 遵循promptx/frontend-developer设计标准
This commit is contained in:
@@ -8,7 +8,8 @@ import {
|
||||
Eye,
|
||||
Edit,
|
||||
Trash2,
|
||||
FolderOpen
|
||||
FolderOpen,
|
||||
CheckCircle
|
||||
} from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { SearchInput } from './InteractiveInput';
|
||||
@@ -28,6 +29,9 @@ interface SegmentWithDetails {
|
||||
duration: number;
|
||||
file_path: string;
|
||||
thumbnail_path?: string;
|
||||
usage_count: number;
|
||||
is_used: boolean;
|
||||
last_used_at?: string;
|
||||
};
|
||||
material_name: string;
|
||||
material_type: string;
|
||||
@@ -198,6 +202,7 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedClassification, setSelectedClassification] = useState<string>('全部');
|
||||
const [selectedModel, setSelectedModel] = useState<string>('全部');
|
||||
const [selectedUsageStatus, setSelectedUsageStatus] = useState<string>('全部');
|
||||
const [thumbnailCache, setThumbnailCache] = useState<Map<string, string>>(new Map());
|
||||
|
||||
// 加载数据
|
||||
@@ -275,6 +280,47 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
return options;
|
||||
}, [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(() => {
|
||||
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()) {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
@@ -310,7 +368,7 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
}
|
||||
|
||||
return segments;
|
||||
}, [segmentView, selectedClassification, selectedModel, searchTerm]);
|
||||
}, [segmentView, selectedClassification, selectedModel, selectedUsageStatus, searchTerm]);
|
||||
|
||||
// 渲染片段卡片
|
||||
const renderSegmentCard = (segment: SegmentWithDetails) => {
|
||||
@@ -396,6 +454,16 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
置信度: {Math.round(segment.classification.confidence * 100)}%
|
||||
</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>
|
||||
@@ -462,9 +530,9 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
</div>
|
||||
|
||||
{/* 筛选条件 */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 animate-fadeIn">
|
||||
{/* 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">
|
||||
<Tag size={16} className="text-gray-600" />
|
||||
<span className="text-sm font-medium text-gray-700">AI分类:</span>
|
||||
@@ -474,13 +542,13 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
<button
|
||||
key={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
|
||||
? 'bg-blue-100 text-blue-800 border border-blue-200'
|
||||
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 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">
|
||||
<span className="ml-1.5 px-1.5 py-0.5 bg-white rounded-full text-xs shadow-sm">
|
||||
{option.count}
|
||||
</span>
|
||||
</button>
|
||||
@@ -489,7 +557,7 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
</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">
|
||||
<Users size={16} className="text-gray-600" />
|
||||
<span className="text-sm font-medium text-gray-700">模特:</span>
|
||||
@@ -499,13 +567,38 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
<button
|
||||
key={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
|
||||
? 'bg-green-100 text-green-800 border border-green-200'
|
||||
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 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">
|
||||
<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}
|
||||
</span>
|
||||
</button>
|
||||
@@ -514,16 +607,16 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
</div>
|
||||
|
||||
{/* 当前筛选条件显示 */}
|
||||
{(selectedClassification !== '全部' || selectedModel !== '全部') && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 rounded-lg">
|
||||
<Filter size={16} className="text-gray-500" />
|
||||
<span className="text-sm text-gray-600">当前筛选:</span>
|
||||
{(selectedClassification !== '全部' || selectedModel !== '全部' || selectedUsageStatus !== '全部') && (
|
||||
<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>
|
||||
{selectedClassification !== '全部' && (
|
||||
<span className="inline-flex items-center px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
|
||||
AI分类: {selectedClassification}
|
||||
</span>
|
||||
)}
|
||||
{selectedClassification !== '全部' && selectedModel !== '全部' && (
|
||||
{(selectedClassification !== '全部' && selectedModel !== '全部') && (
|
||||
<span className="text-xs text-gray-500">AND</span>
|
||||
)}
|
||||
{selectedModel !== '全部' && (
|
||||
@@ -531,13 +624,23 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
||||
模特: {selectedModel}
|
||||
</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
|
||||
onClick={() => {
|
||||
setSelectedClassification('全部');
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export const TemplateMatchingResultManager: React.FC<TemplateMatchingResultManag
|
||||
projectId,
|
||||
templateId,
|
||||
bindingId,
|
||||
showStats = true,
|
||||
showStats = false,
|
||||
onResultSelect,
|
||||
}) => {
|
||||
const [results, setResults] = useState<TemplateMatchingResult[]>([]);
|
||||
|
||||
@@ -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 { 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 { useProjectStore } from '../store/projectStore';
|
||||
import { useMaterialStore } from '../store/materialStore';
|
||||
@@ -129,6 +129,12 @@ export const ProjectDetails: React.FC = () => {
|
||||
const [segmentStats, setSegmentStats] = useState<any>(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) => {
|
||||
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(() => {
|
||||
if (!projects.length) {
|
||||
@@ -172,9 +200,18 @@ export const ProjectDetails: React.FC = () => {
|
||||
loadSegmentStats(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(() => {
|
||||
@@ -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) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
@@ -856,6 +1001,124 @@ export const ProjectDetails: React.FC = () => {
|
||||
{/* 素材管理选项卡 */}
|
||||
{activeTab === 'materials' && (
|
||||
<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 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">
|
||||
<MaterialCardSkeleton count={8} />
|
||||
</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">
|
||||
{materials.map((material) => (
|
||||
{filteredMaterials.map((material) => (
|
||||
<MaterialCard
|
||||
key={material.id}
|
||||
material={material}
|
||||
@@ -886,6 +1149,27 @@ export const ProjectDetails: React.FC = () => {
|
||||
/>
|
||||
))}
|
||||
</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="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>
|
||||
<TemplateMatchingResultManager
|
||||
projectId={project.id}
|
||||
showStats={true}
|
||||
showStats={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -70,6 +70,9 @@ export interface MaterialSegment {
|
||||
file_path: string;
|
||||
file_size: number;
|
||||
thumbnail_path?: string; // 缩略图路径
|
||||
usage_count: number; // 使用次数
|
||||
is_used: boolean; // 是否已使用
|
||||
last_used_at?: string; // 最后使用时间
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user