import React, { useState, useEffect } from 'react'; import { FileVideo, FileAudio, FileImage, File, Clock, ExternalLink, ChevronDown, ChevronUp, Monitor, Volume2, Palette, Calendar, Hash, Zap, HardDrive, Film, Eye, Brain, Loader2, User, Edit2, Trash2, RefreshCw } from 'lucide-react'; import { Material, MaterialSegment } from '../types/material'; import { useMaterialStore } from '../store/materialStore'; import { useVideoClassificationStore } from '../store/videoClassificationStore'; import { Model } from '../types/model'; import { invoke } from '@tauri-apps/api/core'; import { DeleteConfirmDialog } from './DeleteConfirmDialog'; interface MaterialCardProps { material: Material; onEdit?: (material: Material) => void; onDelete?: (materialId: string, materialName: string) => void; onReprocess?: (materialId: string) => void; } // 格式化时间(秒转为 mm:ss 格式) const formatTime = (seconds: number): string => { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; }; // 格式化文件大小 const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // 格式化比特率 const formatBitrate = (bitrate: number): string => { if (bitrate >= 1000000) { return `${(bitrate / 1000000).toFixed(1)} Mbps`; } else if (bitrate >= 1000) { return `${(bitrate / 1000).toFixed(0)} Kbps`; } return `${bitrate} bps`; }; // 格式化分辨率 const formatResolution = (width: number, height: number): string => { return `${width}×${height}`; }; // 格式化日期 const formatDate = (dateString: string): string => { return new Date(dateString).toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; /** * 素材卡片组件 * 显示素材信息和切分片段 */ export const MaterialCard: React.FC = ({ material, onEdit, onDelete, onReprocess }) => { const { getMaterialSegments } = useMaterialStore(); const { startClassification, isLoading: classificationLoading } = useVideoClassificationStore(); const [segments, setSegments] = useState([]); const [showSegments, setShowSegments] = useState(false); const [loadingSegments, setLoadingSegments] = useState(false); const [isClassifying, setIsClassifying] = useState(false); const [associatedModel, setAssociatedModel] = useState(null); const [loadingModel, setLoadingModel] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [isReprocessing, setIsReprocessing] = useState(false); // 获取素材类型图标 const getTypeIcon = (type: string) => { switch (type) { case 'Video': return ; case 'Audio': return ; case 'Image': return ; default: return ; } }; // 获取状态颜色 const getStatusColor = (status: string) => { switch (status) { case 'Completed': return 'text-green-600 bg-green-50'; case 'Processing': return 'text-blue-600 bg-blue-50'; case 'Failed': return 'text-red-600 bg-red-50'; case 'Pending': return 'text-yellow-600 bg-yellow-50'; default: return 'text-gray-600 bg-gray-50'; } }; // 获取关联的模特信息 useEffect(() => { const fetchAssociatedModel = async () => { if (!material.model_id) { setAssociatedModel(null); return; } setLoadingModel(true); try { const model = await invoke('get_model_by_id', { id: material.model_id }); setAssociatedModel(model); } catch (error) { console.error('获取关联模特失败:', error); setAssociatedModel(null); } finally { setLoadingModel(false); } }; fetchAssociatedModel(); }, [material.model_id]); // 加载切分片段 const loadSegments = async () => { if (segments.length > 0) { setShowSegments(!showSegments); return; } setLoadingSegments(true); try { const materialSegments = await getMaterialSegments(material.id); setSegments(materialSegments); setShowSegments(true); } catch (error) { console.error('加载切分片段失败:', error); } finally { setLoadingSegments(false); } }; // 打开文件所在文件夹 const openFileLocation = async (filePath: string) => { try { const { revealItemInDir } = await import('@tauri-apps/plugin-opener'); // 处理 Windows 路径格式,移除 \\?\ 前缀 let normalizedPath = filePath; if (normalizedPath.startsWith('\\\\?\\')) { normalizedPath = normalizedPath.substring(4); } console.log('打开文件位置:', normalizedPath); await revealItemInDir(normalizedPath); } catch (error) { console.error('打开文件位置失败:', error); // 如果 revealItemInDir 失败,尝试打开文件所在目录 try { const { openPath } = await import('@tauri-apps/plugin-opener'); // 获取文件所在目录 const pathParts = filePath.split(/[/\\]/); pathParts.pop(); // 移除文件名 const dirPath = pathParts.join('\\'); let normalizedDirPath = dirPath; if (normalizedDirPath.startsWith('\\\\?\\')) { normalizedDirPath = normalizedDirPath.substring(4); } console.log('尝试打开目录:', normalizedDirPath); await openPath(normalizedDirPath); } catch (fallbackError) { console.error('备用方法也失败:', fallbackError); alert('无法打开文件位置,请检查文件是否存在'); } } }; // 启动AI分类 const handleStartClassification = async () => { if (!material.project_id) { console.error('缺少项目ID'); return; } setIsClassifying(true); try { const request = { material_id: material.id, project_id: material.project_id, overwrite_existing: false, priority: 1, }; const taskIds = await startClassification(request); console.log(`已创建 ${taskIds.length} 个分类任务`); // 可以在这里显示成功消息或打开进度对话框 } catch (error) { console.error('启动AI分类失败:', error); // 可以在这里显示错误消息 } finally { setIsClassifying(false); } }; // 处理删除素材 const handleDeleteClick = () => { setShowDeleteConfirm(true); }; const handleDeleteConfirm = async () => { if (!onDelete) return; setIsDeleting(true); try { await onDelete(material.id, material.name); setShowDeleteConfirm(false); } catch (error) { console.error('删除素材失败:', error); } finally { setIsDeleting(false); } }; const handleDeleteCancel = () => { setShowDeleteConfirm(false); }; // 处理重新处理素材 const handleReprocessClick = async () => { if (!onReprocess) return; setIsReprocessing(true); try { await onReprocess(material.id); } catch (error) { console.error('重新处理素材失败:', error); } finally { setIsReprocessing(false); } }; return (
{/* 素材基本信息 */}
{getTypeIcon(material.material_type)}

{material.name}

{/* 重新处理按钮 - 仅在状态为 Pending 时显示 */} {material.processing_status === 'Pending' && onReprocess && ( )} {onEdit && ( )} {onDelete && ( )} {material.processing_status}
{/* 素材详细信息 */}
{/* 基本信息 */}
{formatFileSize(material.file_size)}
{formatDate(material.created_at)}
{/* 关联模特信息 */} {material.model_id && (
关联模特
{loadingModel ? (
加载中...
) : associatedModel ? (

{associatedModel.name}

{associatedModel.stage_name && (

艺名: {associatedModel.stage_name}

)} {associatedModel.description && (

{associatedModel.description}

)}
) : (

模特信息加载失败

)}
)} {/* 元数据信息 */} {material.metadata !== 'None' && (
{/* 视频元数据 */} {material.metadata && typeof material.metadata === 'object' && 'Video' in material.metadata && (
视频信息
{formatTime(material.metadata.Video.duration)}
{formatResolution(material.metadata.Video.width, material.metadata.Video.height)}
{formatBitrate(material.metadata.Video.bitrate)}
{material.metadata.Video.fps} fps
{material.metadata.Video.codec}
{material.metadata.Video.has_audio && (
{material.metadata.Video.audio_codec || 'Audio'}
)}
)} {/* 音频元数据 */} {material.metadata && typeof material.metadata === 'object' && 'Audio' in material.metadata && (
音频信息
{formatTime(material.metadata.Audio.duration)}
{formatBitrate(material.metadata.Audio.bitrate)}
{material.metadata.Audio.sample_rate} Hz
{material.metadata.Audio.codec}
)} {/* 图片元数据 */} {material.metadata && typeof material.metadata === 'object' && 'Image' in material.metadata && (
图片信息
{formatResolution(material.metadata.Image.width, material.metadata.Image.height)}
{material.metadata.Image.format}
{material.metadata.Image.dpi && (
{material.metadata.Image.dpi} DPI
)}
)}
)} {/* 处理统计信息 */} {(material.scene_detection || material.segments.length > 0) && (
处理统计
{material.scene_detection && (
{material.scene_detection.total_scenes} 个场景
)} {material.segments.length > 0 && (
{material.segments.length} 个片段
)} {material.processed_at && (
已处理
)}
)}
{/* 切分片段控制 */} {material.material_type === 'Video' && material.processing_status === 'Completed' && (
{/* AI智能分类按钮 */}
{/* 切分片段列表 */} {showSegments && segments.length > 0 && (
切分片段 ({segments.length})
{segments.map((segment) => (
#{segment.segment_index + 1} {formatTime(segment.start_time)} - {formatTime(segment.end_time)} ({formatTime(segment.duration)})
))}
)} {showSegments && segments.length === 0 && (
暂无切分片段
)}
)} {/* 删除确认对话框 */}
); };