- 为Material数据模型添加thumbnail_path字段 - 实现get_material_thumbnail_base64 API命令支持Material缩略图生成 - 创建MaterialThumbnail组件,支持懒加载和缓存机制 - 重新设计MaterialCard布局,使用缩略图替换文件类型图标 - 精简MaterialCard信息展示,将详细信息移到可折叠区域 - 优化按钮布局,使界面更加紧凑 - 简化切分片段显示方式,提升用户体验 - 修复数据库DateTime解析问题,支持SQLite和RFC3339两种格式 - 添加数据库迁移支持thumbnail_path字段 - 遵循promptx/tauri-desktop-app-expert开发规范
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
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>
|
||
);
|
||
};
|