From 0898b4b9e2a87472e0c04cab7367eb97ed5c33b0 Mon Sep 17 00:00:00 2001 From: imeepos Date: Tue, 15 Jul 2025 22:01:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BAMaterialSegmentView=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=A7=86=E9=A2=91=E7=89=87=E6=AE=B5=E7=BC=A9=E7=95=A5?= =?UTF-8?q?=E5=9B=BE=E6=98=BE=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缩略图显示功能: - 使用视频首帧生成缩略图:取片段开始时间作为缩略图时间戳 - 智能缓存机制:避免重复生成相同片段的缩略图 - 异步加载:缩略图生成不阻塞界面渲染 - 优雅降级:生成失败时显示默认视频图标 ThumbnailDisplay组件: - 独立的缩略图显示组件,职责单一 - 加载状态指示:显示旋转动画表示正在生成缩略图 - 错误处理:图片加载失败时自动回退到默认图标 - 响应式设计:160x120像素缩略图,适配卡片布局 技术实现: - 利用现有generate_video_thumbnail命令生成缩略图 - Map缓存机制:segmentId -> thumbnailUrl映射 - file://协议:本地文件访问支持 - useEffect钩子:组件挂载时自动加载缩略图 用户体验优化: - 视觉丰富:片段卡片显示实际视频内容预览 - 快速识别:用户可以通过缩略图快速识别视频内容 - 性能优化:缓存机制避免重复生成 - 加载反馈:清楚的加载状态提示 功能特点: - 首帧缩略图:使用片段开始时间的首帧作为预览 - 自动生成:无需手动操作,自动为每个片段生成缩略图 - 内存缓存:同一会话中避免重复生成 - 错误恢复:生成失败时显示默认图标,不影响其他功能 现在MaterialSegmentView提供了更加直观的视觉体验: 1. 每个片段卡片显示实际的视频首帧缩略图 2. 用户可以快速预览视频内容 3. 加载过程有清楚的视觉反馈 4. 生成失败时有优雅的降级处理 --- .../src/components/MaterialSegmentView.tsx | 102 +++++++++++++++++- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/MaterialSegmentView.tsx b/apps/desktop/src/components/MaterialSegmentView.tsx index e5532ee..920865c 100644 --- a/apps/desktop/src/components/MaterialSegmentView.tsx +++ b/apps/desktop/src/components/MaterialSegmentView.tsx @@ -106,6 +106,98 @@ const playVideoSegment = async (filePath: string, startTime: number, endTime: nu } }; +// 生成片段缩略图(使用首帧) +const generateSegmentThumbnail = async (segment: SegmentWithDetails): Promise => { + try { + // 使用片段的开始时间作为缩略图时间戳(首帧) + const timestamp = segment.segment.start_time; + + // 生成缩略图文件名 + const thumbnailFileName = `${segment.segment.id}_thumbnail.jpg`; + const thumbnailPath = `${segment.segment.file_path.replace(/\.[^/.]+$/, '')}_${thumbnailFileName}`; + + await invoke('generate_video_thumbnail', { + inputPath: segment.segment.file_path, + outputPath: thumbnailPath, + timestamp, + width: 160, + height: 120 + }); + + return thumbnailPath; + } 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; + } + + // 生成缩略图 + 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, thumbnailCache, setThumbnailCache, generateSegmentThumbnail]); + + return ( +
+ {loading ? ( +
+ ) : thumbnailUrl ? ( + 视频缩略图 setThumbnailUrl(null)} + /> + ) : ( +
+ ); +}; + /** * 素材片段管理组件 - 多条件检索标签页风格 */ @@ -116,6 +208,7 @@ export const MaterialSegmentView: React.FC = ({ projec const [searchTerm, setSearchTerm] = useState(''); const [selectedClassification, setSelectedClassification] = useState('全部'); const [selectedModel, setSelectedModel] = useState('全部'); + const [thumbnailCache, setThumbnailCache] = useState>(new Map()); // 加载数据 const loadSegmentView = async () => { @@ -240,9 +333,12 @@ export const MaterialSegmentView: React.FC = ({ projec
{/* 缩略图 */} -
-
+ {/* 内容信息 */}