Files
mixvideo-v2/apps/desktop/src/components/MaterialThumbnail.tsx
imeepos c7f9c9f4bb feat: 为MaterialCard添加缩略图功能并优化UI展示
- 为Material数据模型添加thumbnail_path字段
- 实现get_material_thumbnail_base64 API命令支持Material缩略图生成
- 创建MaterialThumbnail组件,支持懒加载和缓存机制
- 重新设计MaterialCard布局,使用缩略图替换文件类型图标
- 精简MaterialCard信息展示,将详细信息移到可折叠区域
- 优化按钮布局,使界面更加紧凑
- 简化切分片段显示方式,提升用户体验
- 修复数据库DateTime解析问题,支持SQLite和RFC3339两种格式
- 添加数据库迁移支持thumbnail_path字段
- 遵循promptx/tauri-desktop-app-expert开发规范
2025-07-16 00:25:08 +08:00

136 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { FileVideo, FileAudio, FileImage, File, Loader2 } from 'lucide-react';
import { Material } from '../types/material';
import { invoke } from '@tauri-apps/api/core';
import { useLazyLoad } from '../hooks/useLazyLoad';
interface MaterialThumbnailProps {
material: Material;
size?: 'small' | 'medium' | 'large';
className?: string;
thumbnailCache?: Map<string, string>;
setThumbnailCache?: (cache: Map<string, string>) => void;
}
/**
* Material缩略图组件
* 遵循Tauri开发规范的组件设计模式
* 支持懒加载、缓存机制、错误处理
*/
export const MaterialThumbnail: React.FC<MaterialThumbnailProps> = ({
material,
size = 'medium',
className = '',
thumbnailCache = new Map(),
setThumbnailCache = () => {},
}) => {
const [loading, setLoading] = useState(false);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(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 getTypeIcon = () => {
const iconSize = size === 'small' ? 'w-6 h-6' : size === 'large' ? 'w-12 h-12' : 'w-8 h-8';
switch (material.material_type) {
case 'Video':
return <FileVideo className={`${iconSize} text-blue-500`} />;
case 'Audio':
return <FileAudio className={`${iconSize} text-green-500`} />;
case 'Image':
return <FileImage className={`${iconSize} text-purple-500`} />;
default:
return <File className={`${iconSize} text-gray-500`} />;
}
};
useEffect(() => {
// 只有当元素可见时才加载缩略图
if (!isVisible) return;
// 只为视频类型生成缩略图
if (material.material_type !== 'Video') return;
const loadThumbnail = async () => {
const materialId = material.id;
// 检查缓存
if (thumbnailCache.has(materialId)) {
const cachedUrl = thumbnailCache.get(materialId);
setThumbnailUrl(cachedUrl || null);
return;
}
// 加载缩略图
setLoading(true);
setError(false);
try {
console.log('获取素材缩略图:', materialId);
const dataUrl = await invoke<string>('get_material_thumbnail_base64', {
materialId: materialId
});
console.log('获取缩略图成功');
setThumbnailUrl(dataUrl);
// 更新缓存
const newCache = new Map(thumbnailCache);
newCache.set(materialId, dataUrl);
setThumbnailCache(newCache);
} catch (error) {
console.error('获取缩略图失败:', error);
setError(true);
} finally {
setLoading(false);
}
};
loadThumbnail();
}, [isVisible, material.id, material.material_type, thumbnailCache, setThumbnailCache]);
return (
<div
ref={elementRef}
className={`${getSizeClasses()} bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden ${className}`}
>
{loading ? (
<Loader2 className={`${size === 'small' ? 'w-3 h-3' : size === 'large' ? 'w-6 h-6' : 'w-4 h-4'} animate-spin text-blue-600`} />
) : thumbnailUrl && !error ? (
<img
src={thumbnailUrl}
alt={`${material.name} 缩略图`}
className="w-full h-full object-cover rounded-lg"
onError={() => {
setError(true);
setThumbnailUrl(null);
}}
/>
) : isVisible ? (
getTypeIcon()
) : (
// 未加载时显示占位符
<div className={`${size === 'small' ? 'w-6 h-6' : size === 'large' ? 'w-12 h-12' : 'w-8 h-8'} bg-gray-200 rounded flex items-center justify-center`}>
<div className={`${size === 'small' ? 'w-3 h-3' : size === 'large' ? 'w-6 h-6' : 'w-4 h-4'} bg-gray-300 rounded`}></div>
</div>
)}
</div>
);
};