From 822bfe6e9cba3aa175099a02237e4cb5151440da Mon Sep 17 00:00:00 2001 From: imeepos Date: Fri, 18 Jul 2025 12:19:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=E6=A8=A1=E6=9D=BF=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E7=89=87=E6=AE=B5=E6=B7=BB=E5=8A=A0=E7=BC=A9=E7=95=A5?= =?UTF-8?q?=E5=9B=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建SegmentThumbnail组件,支持懒加载和缓存 - 修改TemplateMatchingResultDetailModal,集成缩略图显示 - 添加get_material_segment_by_id API命令获取片段详细信息 - 优化片段信息布局,简化显示内容(只显示片段名称和匹配原因) - 支持通过material_segment_id获取和显示实际的片段文件名 --- apps/desktop/src-tauri/src/lib.rs | 1 + .../commands/material_commands.rs | 16 +++ .../src/components/SegmentThumbnail.tsx | 123 ++++++++++++++++++ .../TemplateMatchingResultDetailModal.tsx | 84 +++++++++--- 4 files changed, 208 insertions(+), 16 deletions(-) create mode 100644 apps/desktop/src/components/SegmentThumbnail.tsx diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 0b2e895..6ede251 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -79,6 +79,7 @@ pub fn run() { commands::material_commands::get_segment_thumbnail_base64, commands::material_commands::test_scene_detection, commands::material_commands::get_material_segments, + commands::material_commands::get_material_segment_by_id, commands::material_commands::test_video_split, commands::material_commands::associate_material_to_model, commands::material_commands::disassociate_material_from_model, 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 860c2e0..fb22bbc 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/material_commands.rs @@ -1467,6 +1467,22 @@ pub async fn get_material_segments( .map_err(|e| e.to_string()) } +/// 根据片段ID获取片段详细信息命令 +#[command] +pub async fn get_material_segment_by_id( + state: State<'_, AppState>, + segment_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_segment_by_id_sync(&segment_id) + .map_err(|e| e.to_string()) +} + /// 测试视频切分命令(用于调试不同切分模式) #[command] pub async fn test_video_split( diff --git a/apps/desktop/src/components/SegmentThumbnail.tsx b/apps/desktop/src/components/SegmentThumbnail.tsx new file mode 100644 index 0000000..8dd34ad --- /dev/null +++ b/apps/desktop/src/components/SegmentThumbnail.tsx @@ -0,0 +1,123 @@ +import React, { useState, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { Loader2, Image as ImageIcon } from 'lucide-react'; +import { useLazyLoad } from '../hooks/useLazyLoad'; + +interface SegmentThumbnailProps { + segmentId: string; + size?: 'small' | 'medium' | 'large'; + className?: string; + thumbnailCache?: Map; + setThumbnailCache?: React.Dispatch>>; +} + +/** + * 片段缩略图组件 + * 专门用于显示模板匹配结果中的片段缩略图 + * 遵循Tauri开发规范的组件设计模式 + * 支持懒加载、缓存机制、错误处理 + */ +export const SegmentThumbnail: React.FC = ({ + segmentId, + size = 'medium', + className = '', + thumbnailCache = new Map(), + setThumbnailCache = () => {}, +}) => { + const [loading, setLoading] = useState(false); + const [thumbnailUrl, setThumbnailUrl] = useState(null); + const [error, setError] = useState(false); + + // 使用懒加载Hook,当缩略图容器可见时才开始加载 + const { isVisible, elementRef } = useLazyLoad(0.1, '100px'); + + // 根据size确定尺寸 + const getSizeClasses = () => { + switch (size) { + case 'small': + return 'w-12 h-12'; + case 'medium': + return 'w-16 h-16'; + case 'large': + return 'w-24 h-24'; + default: + return 'w-16 h-16'; + } + }; + + // 获取图标尺寸 + const getIconSize = () => { + switch (size) { + case 'small': + return 'w-3 h-3'; + case 'medium': + return 'w-4 h-4'; + case 'large': + return 'w-6 h-6'; + default: + return 'w-4 h-4'; + } + }; + + useEffect(() => { + // 只有当元素可见时才加载缩略图 + if (!isVisible || !segmentId) return; + + const loadThumbnail = async () => { + // 检查缓存 + if (thumbnailCache.has(segmentId)) { + const cachedUrl = thumbnailCache.get(segmentId); + setThumbnailUrl(cachedUrl || null); + return; + } + + // 加载缩略图 + setLoading(true); + setError(false); + + try { + console.log('获取片段缩略图:', segmentId); + const dataUrl = await invoke('get_segment_thumbnail_base64', { + segmentId: segmentId + }); + console.log('获取缩略图成功'); + setThumbnailUrl(dataUrl); + + // 更新缓存 + const newCache = new Map(thumbnailCache); + newCache.set(segmentId, dataUrl); + setThumbnailCache(newCache); + } catch (error) { + console.error('获取缩略图失败:', error); + setError(true); + } finally { + setLoading(false); + } + }; + + loadThumbnail(); + }, [isVisible, segmentId, thumbnailCache, setThumbnailCache]); + + return ( +
+ {loading ? ( + + ) : thumbnailUrl && !error ? ( + 片段缩略图 { + setError(true); + setThumbnailUrl(null); + }} + /> + ) : ( + + )} +
+ ); +}; diff --git a/apps/desktop/src/components/TemplateMatchingResultDetailModal.tsx b/apps/desktop/src/components/TemplateMatchingResultDetailModal.tsx index a04c296..416ee47 100644 --- a/apps/desktop/src/components/TemplateMatchingResultDetailModal.tsx +++ b/apps/desktop/src/components/TemplateMatchingResultDetailModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { invoke } from '@tauri-apps/api/core'; -import { +import { TemplateMatchingResultDetail, MatchingResultStatus } from '../types/templateMatchingResult'; @@ -8,6 +8,7 @@ import { Modal } from './Modal'; import { LoadingSpinner } from './LoadingSpinner'; import { ErrorMessage } from './ErrorMessage'; import { TabNavigation } from './TabNavigation'; +import { SegmentThumbnail } from './SegmentThumbnail'; interface TemplateMatchingResultDetailModalProps { resultId: string; @@ -24,6 +25,8 @@ export const TemplateMatchingResultDetailModal: React.FC(null); const [activeTab, setActiveTab] = useState('overview'); + const [thumbnailCache, setThumbnailCache] = useState>(new Map()); + const [segmentDetails, setSegmentDetails] = useState>(new Map()); const [editMode, setEditMode] = useState(false); const [editForm, setEditForm] = useState({ result_name: '', @@ -104,6 +107,49 @@ export const TemplateMatchingResultDetailModal: React.FC 0 ? `${minutes}:${remainingSeconds.padStart(4, '0')}` : `${remainingSeconds}s`; }; + // 从文件路径提取文件名 + const extractFileName = (filePath: string): string => { + if (!filePath) return ''; + return filePath.split(/[/\\]/).pop() || filePath; + }; + + // 获取片段详细信息 + const getSegmentDetails = async (segmentId: string) => { + if (segmentDetails.has(segmentId)) { + return segmentDetails.get(segmentId); + } + + try { + const segmentInfo = await invoke('get_material_segment_by_id', { segmentId }); + setSegmentDetails(prev => new Map(prev.set(segmentId, segmentInfo))); + return segmentInfo; + } catch (error) { + console.error('获取片段详情失败:', error); + return null; + } + }; + + // 渲染片段信息组件 + const SegmentInfo: React.FC<{ segment: any }> = ({ segment }) => { + const [segmentDetail, setSegmentDetail] = useState(null); + + useEffect(() => { + const loadSegmentDetail = async () => { + const detail = await getSegmentDetails(segment.material_segment_id); + setSegmentDetail(detail); + }; + loadSegmentDetail(); + }, [segment.material_segment_id]); + + return ( +
+
+ 素材: {segmentDetail ? extractFileName(segmentDetail.file_path) : segment.material_segment_id} +
+
+ ); + }; + // 获取状态样式 const getStatusStyle = (status: MatchingResultStatus) => { switch (status) { @@ -383,22 +429,28 @@ export const TemplateMatchingResultDetailModal: React.FC {segment_results.map((segment) => (
-
-
-

{segment.track_segment_name}

-

- 素材: {segment.material_name} - {segment.model_name && ` | 模特: ${segment.model_name}`} -

-

- 时长: {formatTime(segment.segment_duration)} | - 时间: {formatTime(segment.start_time)} - {formatTime(segment.end_time)} -

-

- 匹配原因: {segment.match_reason} -

+
+ {/* 缩略图 */} + + + {/* 片段信息 */} +
+

{segment.track_segment_name}

+ + + +
+ 匹配原因: {segment.match_reason} +
-
+ + {/* 匹配度 */} +
{(segment.match_score * 100).toFixed(1)}%