first-update
This commit is contained in:
252
easy-dataset-main/components/home/ProjectCard.js
Normal file
252
easy-dataset-main/components/home/ProjectCard.js
Normal file
@@ -0,0 +1,252 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user