feat: 为MaterialSegmentView添加视频片段缩略图显示功能
缩略图显示功能: - 使用视频首帧生成缩略图:取片段开始时间作为缩略图时间戳 - 智能缓存机制:避免重复生成相同片段的缩略图 - 异步加载:缩略图生成不阻塞界面渲染 - 优雅降级:生成失败时显示默认视频图标 ThumbnailDisplay组件: - 独立的缩略图显示组件,职责单一 - 加载状态指示:显示旋转动画表示正在生成缩略图 - 错误处理:图片加载失败时自动回退到默认图标 - 响应式设计:160x120像素缩略图,适配卡片布局 技术实现: - 利用现有generate_video_thumbnail命令生成缩略图 - Map缓存机制:segmentId -> thumbnailUrl映射 - file://协议:本地文件访问支持 - useEffect钩子:组件挂载时自动加载缩略图 用户体验优化: - 视觉丰富:片段卡片显示实际视频内容预览 - 快速识别:用户可以通过缩略图快速识别视频内容 - 性能优化:缓存机制避免重复生成 - 加载反馈:清楚的加载状态提示 功能特点: - 首帧缩略图:使用片段开始时间的首帧作为预览 - 自动生成:无需手动操作,自动为每个片段生成缩略图 - 内存缓存:同一会话中避免重复生成 - 错误恢复:生成失败时显示默认图标,不影响其他功能 现在MaterialSegmentView提供了更加直观的视觉体验: 1. 每个片段卡片显示实际的视频首帧缩略图 2. 用户可以快速预览视频内容 3. 加载过程有清楚的视觉反馈 4. 生成失败时有优雅的降级处理
This commit is contained in:
parent
10177d2501
commit
0898b4b9e2
|
|
@ -106,6 +106,98 @@ const playVideoSegment = async (filePath: string, startTime: number, endTime: nu
|
|||
}
|
||||
};
|
||||
|
||||
// 生成片段缩略图(使用首帧)
|
||||
const generateSegmentThumbnail = async (segment: SegmentWithDetails): Promise<string | null> => {
|
||||
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<string, string>;
|
||||
setThumbnailCache: React.Dispatch<React.SetStateAction<Map<string, string>>>;
|
||||
generateSegmentThumbnail: (segment: SegmentWithDetails) => Promise<string | null>;
|
||||
}
|
||||
|
||||
const ThumbnailDisplay: React.FC<ThumbnailDisplayProps> = ({
|
||||
segment,
|
||||
thumbnailCache,
|
||||
setThumbnailCache,
|
||||
generateSegmentThumbnail
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(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 (
|
||||
<div className="w-20 h-16 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
||||
) : thumbnailUrl ? (
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt="视频缩略图"
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
onError={() => setThumbnailUrl(null)}
|
||||
/>
|
||||
) : (
|
||||
<Video className="w-8 h-8 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 素材片段管理组件 - 多条件检索标签页风格
|
||||
*/
|
||||
|
|
@ -116,6 +208,7 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
|||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedClassification, setSelectedClassification] = useState<string>('全部');
|
||||
const [selectedModel, setSelectedModel] = useState<string>('全部');
|
||||
const [thumbnailCache, setThumbnailCache] = useState<Map<string, string>>(new Map());
|
||||
|
||||
// 加载数据
|
||||
const loadSegmentView = async () => {
|
||||
|
|
@ -240,9 +333,12 @@ export const MaterialSegmentView: React.FC<MaterialSegmentViewProps> = ({ projec
|
|||
<div key={segment.segment.id} className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* 缩略图 */}
|
||||
<div className="w-20 h-16 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<Video className="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
<ThumbnailDisplay
|
||||
segment={segment}
|
||||
thumbnailCache={thumbnailCache}
|
||||
setThumbnailCache={setThumbnailCache}
|
||||
generateSegmentThumbnail={generateSegmentThumbnail}
|
||||
/>
|
||||
|
||||
{/* 内容信息 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
|
|
|
|||
Loading…
Reference in New Issue