feat: 大幅增强ProjectCard - 添加统计信息和打开文件夹功能

功能增强:
为首页项目列表中的ProjectCard添加了丰富的统计信息展示和便捷的文件夹操作功能,
让用户能够快速了解项目状态并方便地访问项目文件。

新增功能:
1. 项目统计信息展示:
   - 自动加载项目素材统计数据
   - 显示素材总数和总文件大小
   - 按类型分类显示(视频、音频、图片、其他)
   - 使用颜色编码区分不同文件类型

2. 打开文件夹功能:
   - 底部按钮栏新增文件夹图标按钮
   - 下拉菜单中添加'打开文件夹'选项
   - 支持Windows长路径格式处理
   - 双重备用机制确保兼容性

3. 加载状态优化:
   - 统计信息加载时显示加载状态
   - 静默处理加载失败,不影响卡片显示
   - 优雅的动画效果

技术实现:
1. 统计数据获取:
   - 使用get_project_material_stats命令
   - React hooks管理状态
   - useEffect自动加载数据

2. 文件夹操作:
   - 集成@tauri-apps/plugin-opener
   - 路径标准化处理
   - 错误处理和用户提示

3. UI设计:
   - 统计信息卡片式展示
   - 图标+数字的直观显示
   - 响应式布局适配

4. 数据格式化:
   - formatFileSize函数处理文件大小
   - 智能单位转换(B/KB/MB/GB/TB)

视觉效果:
 项目统计信息一目了然
 文件类型分布清晰展示
 便捷的文件夹访问按钮
 加载状态友好提示
 颜色编码增强可读性

用户体验:
- 快速了解项目规模和内容
- 一键打开项目文件夹
- 直观的文件类型分布
- 流畅的交互体验

现在首页的项目卡片功能更加完善,用户可以快速了解项目状态并便捷地进行文件管理!
This commit is contained in:
imeepos 2025-07-13 22:26:18 +08:00
parent 822bbd8b64
commit c2e7b2c70f
2 changed files with 156 additions and 12 deletions

View File

@ -1,15 +1,25 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { Project } from '../types/project'; import { Project } from '../types/project';
import { MaterialStats } from '../types/material';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { zhCN } from 'date-fns/locale'; import { zhCN } from 'date-fns/locale';
import { import { invoke } from '@tauri-apps/api/core';
Folder, import {
MoreVertical, Folder,
Edit3, MoreVertical,
Trash2, Edit3,
Trash2,
ExternalLink, ExternalLink,
Calendar, Calendar,
MapPin MapPin,
FolderOpen,
FileVideo,
FileAudio,
FileImage,
File,
HardDrive,
BarChart3,
Hash
} from 'lucide-react'; } from 'lucide-react';
interface ProjectCardProps { interface ProjectCardProps {
@ -19,6 +29,15 @@ interface ProjectCardProps {
onDelete: (id: string) => void; onDelete: (id: string) => void;
} }
// 格式化文件大小
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
/** /**
* *
* *
@ -30,8 +49,30 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
onDelete onDelete
}) => { }) => {
const [showMenu, setShowMenu] = React.useState(false); const [showMenu, setShowMenu] = React.useState(false);
const [stats, setStats] = useState<MaterialStats | null>(null);
const [isLoadingStats, setIsLoadingStats] = useState(false);
const menuRef = React.useRef<HTMLDivElement>(null); const menuRef = React.useRef<HTMLDivElement>(null);
// 加载项目统计信息
useEffect(() => {
const loadStats = async () => {
setIsLoadingStats(true);
try {
const projectStats = await invoke<MaterialStats>('get_project_material_stats', {
projectId: project.id
});
setStats(projectStats);
} catch (error) {
console.error('加载项目统计失败:', error);
// 静默失败,不影响卡片显示
} finally {
setIsLoadingStats(false);
}
};
loadStats();
}, [project.id]);
// 点击外部关闭菜单 // 点击外部关闭菜单
React.useEffect(() => { React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
@ -68,6 +109,37 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
return parts[parts.length - 1] || path; return parts[parts.length - 1] || path;
}; };
// 打开项目文件夹
const handleOpenFolder = async (e: React.MouseEvent) => {
e.stopPropagation();
try {
const { openPath } = await import('@tauri-apps/plugin-opener');
// 处理 Windows 路径格式,移除 \\?\ 前缀
let normalizedPath = project.path;
if (normalizedPath.startsWith('\\\\?\\')) {
normalizedPath = normalizedPath.substring(4);
}
await openPath(normalizedPath);
} catch (error) {
console.error('打开文件夹失败:', error);
// 如果 openPath 失败,尝试使用 revealItemInDir
try {
const { revealItemInDir } = await import('@tauri-apps/plugin-opener');
let normalizedPath = project.path;
if (normalizedPath.startsWith('\\\\?\\')) {
normalizedPath = normalizedPath.substring(4);
}
await revealItemInDir(normalizedPath);
} catch (fallbackError) {
console.error('备用方法也失败:', fallbackError);
alert('无法打开文件夹,请检查路径是否存在');
}
}
};
return ( return (
<div className="card p-6 hover:shadow-lg hover:-translate-y-1 transition-all duration-200 group cursor-pointer animate-slide-up"> <div className="card p-6 hover:shadow-lg hover:-translate-y-1 transition-all duration-200 group cursor-pointer animate-slide-up">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@ -96,6 +168,16 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
<ExternalLink size={14} /> <ExternalLink size={14} />
</button> </button>
<button
className="dropdown-item"
onClick={(e) => {
handleOpenFolder(e);
setShowMenu(false);
}}
>
<FolderOpen size={14} />
</button>
<button <button
className="dropdown-item" className="dropdown-item"
onClick={() => { onClick={() => {
@ -132,6 +214,66 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</p> </p>
)} )}
{/* 项目统计信息 */}
{isLoadingStats ? (
<div className="bg-gray-50 rounded-lg p-3 mb-3">
<div className="flex items-center space-x-1 text-gray-500 text-sm">
<BarChart3 className="w-4 h-4 animate-pulse" />
<span>...</span>
</div>
</div>
) : stats && (
<div className="bg-gray-50 rounded-lg p-3 mb-3 space-y-2">
<div className="flex items-center space-x-1 text-gray-700 font-medium text-sm">
<BarChart3 className="w-4 h-4" />
<span></span>
</div>
{/* 素材统计 */}
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center space-x-1 text-gray-600">
<Hash className="w-3 h-3" />
<span>{stats.total_materials} </span>
</div>
<div className="flex items-center space-x-1 text-gray-600">
<HardDrive className="w-3 h-3" />
<span>{formatFileSize(stats.total_size)}</span>
</div>
</div>
{/* 文件类型分布 */}
{(stats.video_count > 0 || stats.audio_count > 0 || stats.image_count > 0) && (
<div className="flex items-center gap-3 text-xs">
{stats.video_count > 0 && (
<div className="flex items-center space-x-1 text-blue-600">
<FileVideo className="w-3 h-3" />
<span>{stats.video_count}</span>
</div>
)}
{stats.audio_count > 0 && (
<div className="flex items-center space-x-1 text-green-600">
<FileAudio className="w-3 h-3" />
<span>{stats.audio_count}</span>
</div>
)}
{stats.image_count > 0 && (
<div className="flex items-center space-x-1 text-purple-600">
<FileImage className="w-3 h-3" />
<span>{stats.image_count}</span>
</div>
)}
{stats.other_count > 0 && (
<div className="flex items-center space-x-1 text-gray-600">
<File className="w-3 h-3" />
<span>{stats.other_count}</span>
</div>
)}
</div>
)}
</div>
)}
{/* 项目基本信息 */}
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-2 text-xs text-gray-500"> <div className="flex items-center gap-2 text-xs text-gray-500">
<MapPin size={12} /> <MapPin size={12} />
@ -149,6 +291,13 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</div> </div>
<div className="flex gap-2 pt-4 border-t border-gray-100"> <div className="flex gap-2 pt-4 border-t border-gray-100">
<button
className="btn btn-secondary btn-sm"
onClick={handleOpenFolder}
title="打开项目文件夹"
>
<FolderOpen size={14} />
</button>
<button <button
className="btn btn-secondary btn-sm flex-1" className="btn btn-secondary btn-sm flex-1"
onClick={() => onEdit(project)} onClick={() => onEdit(project)}

View File

@ -238,11 +238,6 @@ export const ProjectDetails: React.FC = () => {
{/* 选项卡内容 */} {/* 选项卡内容 */}
{activeTab === 'materials' && ( {activeTab === 'materials' && (
<div> <div>
{/* 素材导入区域 */}
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-6">
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
</div>
{/* 素材列表 */} {/* 素材列表 */}
<div> <div>
<h3 className="text-lg font-medium text-gray-900 mb-3"></h3> <h3 className="text-lg font-medium text-gray-900 mb-3"></h3>