diff --git a/apps/desktop/src/components/MaterialSegmentView.tsx b/apps/desktop/src/components/MaterialSegmentView.tsx new file mode 100644 index 0000000..f32adc2 --- /dev/null +++ b/apps/desktop/src/components/MaterialSegmentView.tsx @@ -0,0 +1,439 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { + Search, + Filter, + Clock, + Tag, + Users, + Play, + Image, + Video, + RefreshCw, + MoreHorizontal, + Eye, + Edit, + Trash2 +} from 'lucide-react'; +import { invoke } from '@tauri-apps/api/core'; +import { SearchInput } from './InteractiveInput'; +import { InteractiveButton } from './InteractiveButton'; + +interface MaterialSegmentViewProps { + projectId: string; +} + +interface SegmentWithDetails { + segment: { + id: string; + material_id: string; + start_time: number; + end_time: number; + duration: number; + file_path: string; + }; + classification_info?: { + category: string; + confidence: number; + reasoning: string; + features: string[]; + product_match: boolean; + quality_score: number; + }; + model_info?: { + id: string; + name: string; + model_type: string; + }; + material_info: { + id: string; + name: string; + file_type: string; + file_size: number; + duration: number; + thumbnail_path?: string; + }; +} + +interface ClassificationGroup { + category: string; + segment_count: number; + total_duration: number; + segments: SegmentWithDetails[]; +} + +interface ModelGroup { + model_id: string; + model_name: string; + segment_count: number; + total_duration: number; + segments: SegmentWithDetails[]; +} + +interface MaterialSegmentView { + project_id: string; + by_classification: ClassificationGroup[]; + by_model: ModelGroup[]; + stats: { + total_segments: number; + classified_segments: number; + unclassified_segments: number; + classification_coverage: number; + classification_counts: Record; + model_counts: Record; + total_duration: number; + }; +} + +/** + * 素材片段管理组件 - 多条件检索标签页风格 + */ +export const MaterialSegmentView: React.FC = ({ projectId }) => { + const [segmentView, setSegmentView] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [activeTab, setActiveTab] = useState<'classification' | 'model'>('classification'); + const [selectedClassification, setSelectedClassification] = useState('全部'); + const [selectedModel, setSelectedModel] = useState('全部'); + + // 加载数据 + const loadSegmentView = async () => { + try { + setLoading(true); + setError(null); + const data = await invoke('get_project_segment_view', { projectId }) as MaterialSegmentView; + setSegmentView(data); + } catch (err) { + console.error('Failed to load segment view:', err); + setError('加载片段数据失败'); + } finally { + setLoading(false); + } + }; + + // 初始加载 + useEffect(() => { + if (projectId) { + loadSegmentView(); + } + }, [projectId]); + + // 格式化时长 + const formatDuration = (seconds: number): string => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + }; + + // 获取分类选项 + const classificationOptions = useMemo(() => { + if (!segmentView) return [{ label: '全部', value: '全部', count: 0 }]; + + const options = [ + { + label: '全部', + value: '全部', + count: segmentView.stats.total_segments + } + ]; + + Object.entries(segmentView.stats.classification_counts).forEach(([category, count]) => { + options.push({ + label: category || '未分类', + value: category || '未分类', + count + }); + }); + + return options; + }, [segmentView]); + + // 获取模特选项 + const modelOptions = useMemo(() => { + if (!segmentView) return [{ label: '全部', value: '全部', count: 0 }]; + + const options = [ + { + label: '全部', + value: '全部', + count: segmentView.stats.total_segments + } + ]; + + Object.entries(segmentView.stats.model_counts).forEach(([modelName, count]) => { + options.push({ + label: modelName || '未指定', + value: modelName || '未指定', + count + }); + }); + + return options; + }, [segmentView]); + + // 获取过滤后的片段 + const filteredSegments = useMemo(() => { + if (!segmentView) return []; + + let segments: SegmentWithDetails[] = []; + + if (activeTab === 'classification') { + if (selectedClassification === '全部') { + // 获取所有片段 + segmentView.by_classification.forEach(group => { + segments.push(...group.segments); + }); + } else { + // 获取特定分类的片段 + const group = segmentView.by_classification.find(g => g.category === selectedClassification); + if (group) { + segments = group.segments; + } + } + } else { + if (selectedModel === '全部') { + // 获取所有片段 + segmentView.by_model.forEach(group => { + segments.push(...group.segments); + }); + } else { + // 获取特定模特的片段 + const group = segmentView.by_model.find(g => g.model_name === selectedModel); + if (group) { + segments = group.segments; + } + } + } + + // 应用搜索过滤 + if (searchTerm.trim()) { + const searchLower = searchTerm.toLowerCase(); + segments = segments.filter(segment => + segment.material_info.name.toLowerCase().includes(searchLower) || + segment.classification_info?.category?.toLowerCase().includes(searchLower) || + segment.model_info?.name?.toLowerCase().includes(searchLower) + ); + } + + return segments; + }, [segmentView, activeTab, selectedClassification, selectedModel, searchTerm]); + + // 渲染片段卡片 + const renderSegmentCard = (segment: SegmentWithDetails) => ( +
+
+ {/* 缩略图 */} +
+ {segment.material_info.thumbnail_path ? ( + 缩略图 + ) : ( +
+ + {/* 内容信息 */} +
+
+
+

+ {segment.material_info.name} +

+

+ {formatDuration(segment.segment.start_time)} - {formatDuration(segment.segment.end_time)} + 时长: {formatDuration(segment.segment.duration)} +

+
+ + {/* 操作按钮 */} +
+ + + +
+
+ + {/* 标签信息 */} +
+ {segment.classification_info && ( + + + {segment.classification_info.category} + + )} + + {segment.model_info && ( + + + {segment.model_info.name} + + )} + + {segment.classification_info?.confidence && ( + + 置信度: {Math.round(segment.classification_info.confidence * 100)}% + + )} +
+
+
+
+ ); + + if (loading) { + return ( +
+
+
+ + 加载片段数据中... +
+
+
+ ); + } + + if (error) { + return ( +
+
+

{error}

+ } + > + 重新加载 + +
+
+ ); + } + + if (!segmentView) { + return ( +
+

暂无片段数据

+
+ ); + } + + return ( +
+ {/* 搜索和刷新 */} +
+
+ +
+ } + > + 刷新 + +
+ + {/* 标签页导航 */} +
+ +
+ + {/* 过滤选项 */} +
+ {activeTab === 'classification' ? ( + classificationOptions.map(option => ( + + )) + ) : ( + modelOptions.map(option => ( + + )) + )} +
+ + {/* 片段列表 */} +
+ {filteredSegments.length > 0 ? ( + filteredSegments.map(segment => renderSegmentCard(segment)) + ) : ( +
+
+
+
+ )} +
+
+ ); +};