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:
parent
822bbd8b64
commit
c2e7b2c70f
|
|
@ -1,15 +1,25 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Project } from '../types/project';
|
||||
import { MaterialStats } from '../types/material';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
import {
|
||||
Folder,
|
||||
MoreVertical,
|
||||
Edit3,
|
||||
Trash2,
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import {
|
||||
Folder,
|
||||
MoreVertical,
|
||||
Edit3,
|
||||
Trash2,
|
||||
ExternalLink,
|
||||
Calendar,
|
||||
MapPin
|
||||
MapPin,
|
||||
FolderOpen,
|
||||
FileVideo,
|
||||
FileAudio,
|
||||
FileImage,
|
||||
File,
|
||||
HardDrive,
|
||||
BarChart3,
|
||||
Hash
|
||||
} from 'lucide-react';
|
||||
|
||||
interface ProjectCardProps {
|
||||
|
|
@ -19,6 +29,15 @@ interface ProjectCardProps {
|
|||
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
|
||||
}) => {
|
||||
const [showMenu, setShowMenu] = React.useState(false);
|
||||
const [stats, setStats] = useState<MaterialStats | null>(null);
|
||||
const [isLoadingStats, setIsLoadingStats] = useState(false);
|
||||
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(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
|
|
@ -68,6 +109,37 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
|
|||
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 (
|
||||
<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">
|
||||
|
|
@ -96,6 +168,16 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
|
|||
<ExternalLink size={14} />
|
||||
打开项目
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
handleOpenFolder(e);
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
打开文件夹
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
|
|
@ -132,6 +214,66 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
|
|||
</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="flex items-center gap-2 text-xs text-gray-500">
|
||||
<MapPin size={12} />
|
||||
|
|
@ -149,6 +291,13 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
|
|||
</div>
|
||||
|
||||
<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
|
||||
className="btn btn-secondary btn-sm flex-1"
|
||||
onClick={() => onEdit(project)}
|
||||
|
|
|
|||
|
|
@ -238,11 +238,6 @@ export const ProjectDetails: React.FC = () => {
|
|||
{/* 选项卡内容 */}
|
||||
{activeTab === 'materials' && (
|
||||
<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>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3">项目素材</h3>
|
||||
|
|
|
|||
Loading…
Reference in New Issue