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开发规范
This commit is contained in:
imeepos
2025-07-16 00:25:08 +08:00
parent b6c85901ce
commit c7f9c9f4bb
10 changed files with 548 additions and 122 deletions

View File

@@ -0,0 +1,135 @@
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>
);
};