diff --git a/apps/desktop/src/components/MaterialSegmentView.tsx b/apps/desktop/src/components/MaterialSegmentView.tsx index d76a209..0967af4 100644 --- a/apps/desktop/src/components/MaterialSegmentView.tsx +++ b/apps/desktop/src/components/MaterialSegmentView.tsx @@ -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 = ({ projec const [searchTerm, setSearchTerm] = useState(''); const [selectedClassification, setSelectedClassification] = useState('全部'); const [selectedModel, setSelectedModel] = useState('全部'); + const [selectedUsageStatus, setSelectedUsageStatus] = useState('全部'); const [thumbnailCache, setThumbnailCache] = useState>(new Map()); // 加载数据 @@ -275,6 +280,47 @@ export const MaterialSegmentView: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ projec 置信度: {Math.round(segment.classification.confidence * 100)}% )} + + {/* 使用状态标识 */} + + + {segment.segment.is_used ? `已使用 (${segment.segment.usage_count}次)` : '未使用'} + @@ -462,9 +530,9 @@ export const MaterialSegmentView: React.FC = ({ projec {/* 筛选条件 */} -
+
{/* AI分类筛选 - 单行显示 */} -
+
AI分类: @@ -474,13 +542,13 @@ export const MaterialSegmentView: React.FC = ({ projec @@ -489,7 +557,7 @@ export const MaterialSegmentView: React.FC = ({ projec
{/* 模特筛选 - 单行显示 */} -
+
模特: @@ -499,13 +567,38 @@ export const MaterialSegmentView: React.FC = ({ projec + ))} +
+
+ + {/* 使用状态筛选 - 单行显示 */} +
+
+ + 是否使用: +
+
+ {usageStatusOptions.map(option => ( + @@ -514,16 +607,16 @@ export const MaterialSegmentView: React.FC = ({ projec
{/* 当前筛选条件显示 */} - {(selectedClassification !== '全部' || selectedModel !== '全部') && ( -
- - 当前筛选: + {(selectedClassification !== '全部' || selectedModel !== '全部' || selectedUsageStatus !== '全部') && ( +
+ + 当前筛选: {selectedClassification !== '全部' && ( AI分类: {selectedClassification} )} - {selectedClassification !== '全部' && selectedModel !== '全部' && ( + {(selectedClassification !== '全部' && selectedModel !== '全部') && ( AND )} {selectedModel !== '全部' && ( @@ -531,13 +624,23 @@ export const MaterialSegmentView: React.FC = ({ projec 模特: {selectedModel} )} + {((selectedClassification !== '全部' || selectedModel !== '全部') && selectedUsageStatus !== '全部') && ( + AND + )} + {selectedUsageStatus !== '全部' && ( + + 使用状态: {selectedUsageStatus} + + )}
diff --git a/apps/desktop/src/components/TemplateMatchingResultManager.tsx b/apps/desktop/src/components/TemplateMatchingResultManager.tsx index 1d9b506..957b15f 100644 --- a/apps/desktop/src/components/TemplateMatchingResultManager.tsx +++ b/apps/desktop/src/components/TemplateMatchingResultManager.tsx @@ -27,7 +27,7 @@ export const TemplateMatchingResultManager: React.FC { const [results, setResults] = useState([]); diff --git a/apps/desktop/src/pages/ProjectDetails.tsx b/apps/desktop/src/pages/ProjectDetails.tsx index ddab99b..9e5e2d3 100644 --- a/apps/desktop/src/pages/ProjectDetails.tsx +++ b/apps/desktop/src/pages/ProjectDetails.tsx @@ -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(null); const [currentMatchingBinding, setCurrentMatchingBinding] = useState(null); + // 素材筛选状态 + const [materialClassificationFilter, setMaterialClassificationFilter] = useState('全部'); + const [materialModelFilter, setMaterialModelFilter] = useState('全部'); + const [materialUsageFilter, setMaterialUsageFilter] = useState('全部'); + 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 (
@@ -856,6 +1001,124 @@ export const ProjectDetails: React.FC = () => { {/* 素材管理选项卡 */} {activeTab === 'materials' && (
+ {/* 素材筛选条件 */} +
+ {/* AI分类筛选 */} +
+
+ + AI分类: +
+
+ {materialClassificationOptions.map(option => ( + + ))} +
+
+ + {/* 模特筛选 */} +
+
+ + 模特: +
+
+ {materialModelOptions.map(option => ( + + ))} +
+
+ + {/* 使用状态筛选 */} +
+
+ + 是否使用: +
+
+ {materialUsageOptions.map(option => ( + + ))} +
+
+ + {/* 当前筛选条件显示 */} + {(materialClassificationFilter !== '全部' || materialModelFilter !== '全部' || materialUsageFilter !== '全部') && ( +
+ + 当前筛选: + {materialClassificationFilter !== '全部' && ( + + AI分类: {materialClassificationFilter} + + )} + {((materialClassificationFilter !== '全部') && (materialModelFilter !== '全部')) && ( + AND + )} + {materialModelFilter !== '全部' && ( + + 模特: {materialModelFilter} + + )} + {((materialClassificationFilter !== '全部' || materialModelFilter !== '全部') && materialUsageFilter !== '全部') && ( + AND + )} + {materialUsageFilter !== '全部' && ( + + 使用状态: {materialUsageFilter} + + )} + +
+ )} +
+ {/* 素材列表 */}
@@ -873,9 +1136,9 @@ export const ProjectDetails: React.FC = () => {
- ) : materials.length > 0 ? ( + ) : filteredMaterials.length > 0 ? (
- {materials.map((material) => ( + {filteredMaterials.map((material) => ( { /> ))}
+ ) : materials.length > 0 ? ( +
+
+ +
+

没有找到匹配的素材

+

+ 尝试调整筛选条件或清除所有筛选来查看更多素材 +

+ +
) : (
@@ -972,7 +1256,7 @@ export const ProjectDetails: React.FC = () => {
)} diff --git a/apps/desktop/src/types/material.ts b/apps/desktop/src/types/material.ts index 5085cae..054bf27 100644 --- a/apps/desktop/src/types/material.ts +++ b/apps/desktop/src/types/material.ts @@ -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; }