import React, { useEffect, useState, useMemo } from 'react'; import { Filter, Tag, Users, Video, RefreshCw, Eye, Edit, Trash2, FolderOpen } 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; thumbnail_path?: string; }; material_name: string; material_type: string; classification?: { category: string; confidence: number; reasoning: string; features: string[]; product_match: boolean; quality_score: number; }; model?: { id: string; name: string; model_type: 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; }; } // 提取文件名的工具函数 const extractFileName = (filePath: string): string => { if (!filePath) return '未知文件'; // 处理Windows路径格式,包括长路径前缀 const cleanPath = filePath.replace(/^\\\\\?\\/, ''); const parts = cleanPath.split(/[\\\/]/); return parts[parts.length - 1] || '未知文件'; }; // 打开文件所在目录 const openFileDirectory = async (filePath: string) => { try { await invoke('open_file_directory', { filePath }); } catch (error) { console.error('打开目录失败:', error); } }; // 播放视频片段 const playVideoSegment = async (filePath: string, startTime: number, endTime: number) => { try { await invoke('play_video_segment', { filePath, startTime, endTime }); } catch (error) { console.error('播放视频失败:', error); } }; // 生成片段缩略图(使用首帧) const generateSegmentThumbnail = async (segment: SegmentWithDetails): Promise => { try { const result = await invoke('generate_and_save_segment_thumbnail', { segmentId: segment.segment.id }); return result; } catch (error) { console.error('生成缩略图失败:', error); return null; } }; // 缩略图显示组件 interface ThumbnailDisplayProps { segment: SegmentWithDetails; thumbnailCache: Map; setThumbnailCache: React.Dispatch>>; generateSegmentThumbnail: (segment: SegmentWithDetails) => Promise; } const ThumbnailDisplay: React.FC = ({ segment, thumbnailCache, setThumbnailCache, generateSegmentThumbnail }) => { const [loading, setLoading] = useState(false); const [thumbnailUrl, setThumbnailUrl] = useState(null); useEffect(() => { const loadThumbnail = async () => { const segmentId = segment.segment.id; // 检查缓存 if (thumbnailCache.has(segmentId)) { setThumbnailUrl(thumbnailCache.get(segmentId) || null); return; } // 首先检查数据库中是否已有缩略图 if (segment.segment.thumbnail_path) { const thumbnailUrl = `file://${segment.segment.thumbnail_path}`; setThumbnailUrl(thumbnailUrl); // 更新缓存 setThumbnailCache(prev => new Map(prev.set(segmentId, thumbnailUrl))); return; } // 如果数据库中没有缩略图,则生成新的 setLoading(true); try { const thumbnailPath = await generateSegmentThumbnail(segment); if (thumbnailPath) { // 转换为可访问的URL const thumbnailUrl = `file://${thumbnailPath}`; setThumbnailUrl(thumbnailUrl); // 更新缓存 setThumbnailCache(prev => new Map(prev.set(segmentId, thumbnailUrl))); } } catch (error) { console.error('加载缩略图失败:', error); } finally { setLoading(false); } }; loadThumbnail(); }, [segment.segment.id, segment.segment.thumbnail_path, thumbnailCache, setThumbnailCache, generateSegmentThumbnail]); return (
{loading ? (
) : thumbnailUrl ? ( 视频缩略图 setThumbnailUrl(null)} /> ) : (
); }; /** * 素材片段管理组件 - 多条件检索标签页风格 */ 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 [selectedClassification, setSelectedClassification] = useState('全部'); const [selectedModel, setSelectedModel] = useState('全部'); const [thumbnailCache, setThumbnailCache] = useState>(new Map()); // 加载数据 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[] = []; segmentView.by_classification.forEach(group => { segments.push(...group.segments); }); // 应用分类过滤 if (selectedClassification !== '全部') { segments = segments.filter(segment => segment.classification?.category === selectedClassification ); } // 应用模特过滤 if (selectedModel !== '全部') { segments = segments.filter(segment => segment.model?.name === selectedModel ); } // 应用搜索过滤 if (searchTerm.trim()) { const searchLower = searchTerm.toLowerCase(); segments = segments.filter(segment => segment.material_name.toLowerCase().includes(searchLower) || segment.classification?.category?.toLowerCase().includes(searchLower) || segment.model?.name?.toLowerCase().includes(searchLower) ); } return segments; }, [segmentView, selectedClassification, selectedModel, searchTerm]); // 渲染片段卡片 const renderSegmentCard = (segment: SegmentWithDetails) => { // 安全检查 if (!segment || !segment.segment || !segment.material_name) { return null; } return (
{/* 缩略图 */} {/* 内容信息 */}

{extractFileName(segment.segment.file_path)}

{segment.segment.file_path && ( )}

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

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

{error}

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

暂无片段数据

); } return (
{/* 搜索和刷新 */}
} > 刷新
{/* 筛选条件 */}
{/* AI分类筛选 - 单行显示 */}
AI分类:
{classificationOptions.map(option => ( ))}
{/* 模特筛选 - 单行显示 */}
模特:
{modelOptions.map(option => ( ))}
{/* 当前筛选条件显示 */} {(selectedClassification !== '全部' || selectedModel !== '全部') && (
当前筛选: {selectedClassification !== '全部' && ( AI分类: {selectedClassification} )} {selectedClassification !== '全部' && selectedModel !== '全部' && ( AND )} {selectedModel !== '全部' && ( 模特: {selectedModel} )}
)}
{/* 片段列表 */}
{filteredSegments.length > 0 ? ( filteredSegments.map(segment => renderSegmentCard(segment)) ) : (
)}
); };