168 lines
4.6 KiB
TypeScript
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>
|
|
);
|
|
};
|