mixvideo-v2/apps/desktop/src/components/ProjectCard.tsx

168 lines
4.6 KiB
TypeScript

import React from 'react';
import { Project } from '../types/project';
import { formatDistanceToNow } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import {
Folder,
MoreVertical,
Edit3,
Trash2,
ExternalLink,
Calendar,
MapPin
} from 'lucide-react';
interface ProjectCardProps {
project: Project;
onOpen: (project: Project) => void;
onEdit: (project: Project) => void;
onDelete: (id: string) => void;
}
/**
* 项目卡片组件
* 遵循简洁大方的设计风格
*/
export const ProjectCard: React.FC<ProjectCardProps> = ({
project,
onOpen,
onEdit,
onDelete
}) => {
const [showMenu, setShowMenu] = React.useState(false);
const menuRef = React.useRef<HTMLDivElement>(null);
// 点击外部关闭菜单
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setShowMenu(false);
}
};
if (showMenu) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showMenu]);
// 格式化时间
const formatTime = (dateString: string) => {
try {
const date = new Date(dateString);
return formatDistanceToNow(date, {
addSuffix: true,
locale: zhCN
});
} catch {
return '未知时间';
}
};
// 获取项目目录名
const getDirectoryName = (path: string) => {
const parts = path.split(/[/\\]/);
return parts[parts.length - 1] || path;
};
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">
<div className="text-primary-600 group-hover:text-primary-700 transition-colors duration-200">
<Folder size={24} />
</div>
<div className="relative" ref={menuRef}>
<button
className="p-1 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors duration-200 opacity-0 group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setShowMenu(!showMenu);
}}
>
<MoreVertical size={16} />
</button>
{showMenu && (
<div className="dropdown-menu animate-fade-in">
<button
className="dropdown-item"
onClick={() => {
onOpen(project);
setShowMenu(false);
}}
>
<ExternalLink size={14} />
</button>
<button
className="dropdown-item"
onClick={() => {
onEdit(project);
setShowMenu(false);
}}
>
<Edit3 size={14} />
</button>
<button
className="dropdown-item danger"
onClick={() => {
onDelete(project.id);
setShowMenu(false);
}}
>
<Trash2 size={14} />
</button>
</div>
)}
</div>
</div>
<div className="mb-4" onClick={() => onOpen(project)}>
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-1">
{project.name}
</h3>
{project.description && (
<p className="text-sm text-gray-600 mb-3 line-clamp-2">
{project.description}
</p>
)}
<div className="space-y-1">
<div className="flex items-center gap-2 text-xs text-gray-500">
<MapPin size={12} />
<span className="truncate" title={project.path}>
{getDirectoryName(project.path)}
</span>
</div>
<div className="flex items-center gap-2 text-xs text-gray-500">
<Calendar size={12} />
<span>
{formatTime(project.updated_at)}
</span>
</div>
</div>
</div>
<div className="flex gap-2 pt-4 border-t border-gray-100">
<button
className="btn btn-secondary btn-sm flex-1"
onClick={() => onEdit(project)}
>
</button>
<button
className="btn btn-primary btn-sm flex-1"
onClick={() => onOpen(project)}
>
</button>
</div>
</div>
);
};