Files

253 lines
7.6 KiB
JavaScript
Raw Permalink Normal View History

2026-03-17 14:36:31 +08:00
'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>
);
}