Files
YG-Datasets/easy-dataset-main/components/home/ProjectCard.js

253 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import {
Card,
Box,
CardActionArea,
CardContent,
Typography,
Avatar,
Divider,
IconButton,
Menu,
MenuItem,
ListItemIcon
} from '@mui/material';
import Link from 'next/link';
import { styles } from '@/styles/home';
import { useTheme, alpha } from '@mui/material/styles';
import DataObjectIcon from '@mui/icons-material/DataObject';
import DeleteIcon from '@mui/icons-material/Delete';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import TokenIcon from '@mui/icons-material/Token';
import AssessmentIcon from '@mui/icons-material/Assessment';
import QuizIcon from '@mui/icons-material/Quiz';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
/**
* 统计项组件
*/
const StatItem = ({ icon: Icon, count, label, color, isToken }) => {
const theme = useTheme();
// 格式化数字
const displayCount = isToken ? (count || 0).toLocaleString() : count || 0;
return (
<Box sx={styles.statItem(theme)}>
<Box sx={styles.statIconBox(theme, color)}>
<Icon sx={{ fontSize: 18 }} />
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Typography variant="subtitle2" fontWeight="700" sx={{ lineHeight: 1 }}>
{displayCount}
</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ fontSize: '0.7rem' }}>
{label}
</Typography>
</Box>
</Box>
);
};
/**
* 项目卡片组件
* @param {Object} props - 组件属性
* @param {Object} props.project - 项目数据
* @param {Function} props.onDeleteClick - 删除按钮点击事件处理函数
*/
export default function ProjectCard({ project, onDeleteClick }) {
const { t } = useTranslation();
const theme = useTheme();
const [processingId, setProcessingId] = useState(false);
// 菜单状态
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
// 打开项目目录
const handleOpenDirectory = async event => {
event.stopPropagation();
event.preventDefault();
if (processingId) return;
try {
setProcessingId(true);
const response = await fetch('/api/projects/open-directory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ projectId: project.id })
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || t('migration.openDirectoryFailed'));
}
// 成功打开目录,不需要特别处理
} catch (error) {
console.error('打开目录错误:', error);
alert(error.message);
} finally {
setProcessingId(false);
}
};
// 处理菜单打开
const handleMenuClick = event => {
event.stopPropagation();
event.preventDefault();
setAnchorEl(event.currentTarget);
};
// 处理菜单关闭
const handleMenuClose = event => {
if (event) {
event.stopPropagation();
event.preventDefault();
}
setAnchorEl(null);
};
// 处理打开目录点击
const handleOpenDirectoryClick = event => {
handleMenuClose(event);
handleOpenDirectory(event);
};
// 处理删除点击
const handleDeleteClick = event => {
handleMenuClose(event);
onDeleteClick(event, project);
};
return (
<Card sx={styles.projectCard(theme)}>
<Link
href={`/projects/${project.id}`}
passHref
style={{ textDecoration: 'none', color: 'inherit', height: '100%' }}
>
<CardActionArea component="div" sx={{ height: '100%' }}>
<CardContent sx={styles.projectCardContent}>
{/* 头部Avatar + Title + Menu */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1.5 }}>
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', overflow: 'hidden', flex: 1 }}>
<Avatar
sx={{
bgcolor: alpha(theme.palette.primary.main, 0.1),
color: theme.palette.primary.main,
width: 40,
height: 40,
fontSize: '1.1rem',
fontWeight: 'bold',
borderRadius: '10px'
}}
>
{project.name.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ overflow: 'hidden', flex: 1 }}>
<Typography variant="h6" sx={styles.projectTitle}>
{project.name}
</Typography>
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.7rem' }}>
ID: {project.id}
</Typography>
</Box>
</Box>
<IconButton
size="small"
onClick={handleMenuClick}
sx={{
color: 'text.secondary',
padding: '4px',
'&:hover': { color: 'primary.main', bgcolor: alpha(theme.palette.primary.main, 0.1) }
}}
>
<MoreVertIcon fontSize="small" />
</IconButton>
</Box>
{/* 描述 */}
<Typography variant="body2" sx={styles.projectDescription}>
{project.description || t('projects.noDescription', { defaultValue: '暂无描述' })}
</Typography>
{/* 统计数据 */}
<Box sx={styles.statsContainer}>
<StatItem
icon={QuizIcon}
count={project._count.Questions}
label={t('projects.questions')}
color="primary"
/>
<StatItem
icon={DataObjectIcon}
count={(project._count.ImageDatasets || 0) + (project._count.Datasets || 0)}
label={t('projects.datasets')}
color="secondary"
/>
<StatItem
icon={AssessmentIcon}
count={project._count.EvalDatasets}
label={t('projects.evalDatasets')}
color="info"
/>
<StatItem
icon={TokenIcon}
count={project.totalTokens}
label={t('projects.tokens')}
color="success"
isToken
/>
</Box>
</CardContent>
</CardActionArea>
</Link>
{/* 操作菜单 */}
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleMenuClose}
onClick={e => {
e.preventDefault();
e.stopPropagation();
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{
elevation: 3,
sx: {
borderRadius: '12px',
minWidth: 160,
mt: 0.5
}
}}
>
<MenuItem onClick={handleOpenDirectoryClick}>
<ListItemIcon>
<FolderOpenIcon fontSize="small" />
</ListItemIcon>
<Typography variant="body2">{t('projects.openDirectory')}</Typography>
</MenuItem>
<Divider sx={{ my: 0.5, opacity: 0.5 }} />
<MenuItem onClick={handleDeleteClick} sx={{ color: 'error.main' }}>
<ListItemIcon>
<DeleteIcon fontSize="small" color="error" />
</ListItemIcon>
<Typography variant="body2">{t('common.delete')}</Typography>
</MenuItem>
</Menu>
</Card>
);
}