From 704e6d8fff59456597b3ab1ab657b853d9a2f809 Mon Sep 17 00:00:00 2001 From: imeepos Date: Sun, 13 Jul 2025 21:12:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E5=88=87=E5=88=86=E7=89=87=E6=AE=B5=E6=9F=A5=E7=9C=8B=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: 1. MaterialCard组件 - 增强的素材卡片 - 显示素材基本信息和状态 - 支持展开查看切分片段详情 - 片段时间格式化显示 - 文件位置快速访问 2. get_material_segments命令 - 获取指定素材的所有切分片段 - 返回完整的片段信息包括文件路径 3. 前端store集成 - getMaterialSegments方法 - 错误处理和加载状态 4. 用户界面改进 - 状态颜色编码(完成/处理中/失败/等待) - 可折叠的片段列表 - 时间格式化显示(分:秒) - 片段索引和时长信息 视频切分结果保存位置: - 文件系统: 原视频路径_segments/原视频名_001.mp4 - 数据库: material_segments表存储片段元信息 - 前端: 通过MaterialCard组件可视化查看 现在用户可以: 查看每个素材的切分状态 展开查看具体的切分片段 了解每个片段的时间范围和文件位置 快速访问切分后的文件 --- apps/desktop/src-tauri/src/lib.rs | 3 +- .../commands/material_commands.rs | 16 ++ apps/desktop/src/components/MaterialCard.tsx | 157 ++++++++++++++++++ apps/desktop/src/pages/ProjectDetails.tsx | 18 +- apps/desktop/src/store/materialStore.ts | 11 ++ 5 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 apps/desktop/src/components/MaterialCard.tsx diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 38d75e1..447fb11 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -57,7 +57,8 @@ pub fn run() { commands::material_commands::extract_file_metadata, commands::material_commands::detect_video_scenes, commands::material_commands::generate_video_thumbnail, - commands::material_commands::test_scene_detection + commands::material_commands::test_scene_detection, + commands::material_commands::get_material_segments ]) .setup(|app| { // 初始化应用状态 diff --git a/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs index 02a1ec5..24df0e9 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs @@ -341,3 +341,19 @@ pub async fn test_scene_detection(file_path: String) -> Result { Ok(result) } + +/// 获取素材的切分片段信息命令 +#[command] +pub async fn get_material_segments( + state: State<'_, AppState>, + material_id: String, +) -> Result, String> { + let repository_guard = state.get_material_repository() + .map_err(|e| format!("获取素材仓库失败: {}", e))?; + + let repository = repository_guard.as_ref() + .ok_or("素材仓库未初始化")?; + + repository.get_segments(&material_id) + .map_err(|e| e.to_string()) +} diff --git a/apps/desktop/src/components/MaterialCard.tsx b/apps/desktop/src/components/MaterialCard.tsx new file mode 100644 index 0000000..ee9cf3a --- /dev/null +++ b/apps/desktop/src/components/MaterialCard.tsx @@ -0,0 +1,157 @@ +import React, { useState, useEffect } from 'react'; +import { FileVideo, FileAudio, FileImage, File, Clock, ExternalLink, ChevronDown, ChevronUp } from 'lucide-react'; +import { Material, MaterialSegment } from '../types/material'; +import { useMaterialStore } from '../store/materialStore'; + +interface MaterialCardProps { + material: Material; +} + +/** + * 素材卡片组件 + * 显示素材信息和切分片段 + */ +export const MaterialCard: React.FC = ({ material }) => { + const { getMaterialSegments } = useMaterialStore(); + const [segments, setSegments] = useState([]); + const [showSegments, setShowSegments] = useState(false); + const [loadingSegments, setLoadingSegments] = 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'; + } + }; + + // 格式化时间 + const formatTime = (seconds: number) => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + }; + + // 加载切分片段 + 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 = (filePath: string) => { + // 这里可以调用 Tauri 的 API 来打开文件夹 + console.log('打开文件位置:', filePath); + }; + + return ( +
+ {/* 素材基本信息 */} +
+
+ {getTypeIcon(material.material_type)} +

{material.name}

+
+ + {material.processing_status} + +
+ + {/* 素材详细信息 */} +
+

类型: {material.material_type}

+

大小: {(material.file_size / 1024 / 1024).toFixed(2)} MB

+ {material.segments && material.segments.length > 0 && ( +

片段数: {material.segments.length}

+ )} +
+ + {/* 切分片段控制 */} + {material.material_type === 'Video' && material.processing_status === 'Completed' && ( +
+ + + {/* 切分片段列表 */} + {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 && ( +
+ 暂无切分片段 +
+ )} +
+ )} +
+ ); +}; diff --git a/apps/desktop/src/pages/ProjectDetails.tsx b/apps/desktop/src/pages/ProjectDetails.tsx index 943abed..4360c8b 100644 --- a/apps/desktop/src/pages/ProjectDetails.tsx +++ b/apps/desktop/src/pages/ProjectDetails.tsx @@ -1,14 +1,15 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { ArrowLeft, FolderOpen, Calendar, Settings, Upload } from 'lucide-react'; +import { ArrowLeft, FolderOpen, Calendar, Settings, Upload, FileVideo, Clock, ExternalLink } from 'lucide-react'; import { useProjectStore } from '../store/projectStore'; import { useMaterialStore } from '../store/materialStore'; import { Project } from '../types/project'; -import { MaterialImportResult } from '../types/material'; +import { MaterialImportResult, Material, MaterialSegment } from '../types/material'; import { LoadingSpinner } from '../components/LoadingSpinner'; import { ErrorMessage } from '../components/ErrorMessage'; import { MaterialImportDialog } from '../components/MaterialImportDialog'; import { FFmpegDebugPanel } from '../components/FFmpegDebugPanel'; +import { MaterialCard } from '../components/MaterialCard'; /** * 项目详情页面组件 @@ -240,18 +241,7 @@ export const ProjectDetails: React.FC = () => { ) : materials.length > 0 ? (
{materials.map((material) => ( -
-

{material.name}

-

- 类型: {material.material_type} -

-

- 状态: {material.processing_status} -

-

- 大小: {(material.file_size / 1024 / 1024).toFixed(2)} MB -

-
+ ))}
) : ( diff --git a/apps/desktop/src/store/materialStore.ts b/apps/desktop/src/store/materialStore.ts index d9886ee..6c99175 100644 --- a/apps/desktop/src/store/materialStore.ts +++ b/apps/desktop/src/store/materialStore.ts @@ -313,4 +313,15 @@ export const useMaterialStore = create((set, get) => ({ return `获取状态失败: ${error}`; } }, + + // 获取素材的切分片段 + getMaterialSegments: async (materialId: string) => { + try { + const segments = await invoke('get_material_segments', { materialId }); + return segments; + } catch (error) { + console.error('获取素材片段失败:', error); + return []; + } + }, }));