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:
135
apps/desktop/src/components/MaterialThumbnail.tsx
Normal file
135
apps/desktop/src/components/MaterialThumbnail.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user