253 lines
7.6 KiB
JavaScript
253 lines
7.6 KiB
JavaScript
'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>
|
||
);
|
||
}
|