first-update
This commit is contained in:
173
easy-dataset-main/components/home/CreateProjectDialog.js
Normal file
173
easy-dataset-main/components/home/CreateProjectDialog.js
Normal file
@@ -0,0 +1,173 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem
|
||||
} from '@mui/material';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function CreateProjectDialog({ open, onClose }) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
reuseConfigFrom: ''
|
||||
});
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// 获取项目列表
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/projects');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setProjects(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取项目列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
const handleChange = e => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/projects', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(t('projects.createFailed'));
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
router.push(`/projects/${data.id}/settings?tab=model`);
|
||||
} catch (err) {
|
||||
console.error(t('projects.createError'), err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: '16px',
|
||||
background: theme.palette.mode === 'dark' ? 'rgba(30, 30, 30, 0.9)' : 'rgba(255, 255, 255, 0.9)',
|
||||
backdropFilter: 'blur(8px)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
{t('projects.createNew')}
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogContent>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<TextField
|
||||
name="name"
|
||||
label={t('projects.name')}
|
||||
fullWidth
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
name="description"
|
||||
label={t('projects.description')}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||
<InputLabel id="reuse-config-label">{t('projects.reuseConfig')}</InputLabel>
|
||||
<Select
|
||||
labelId="reuse-config-label"
|
||||
name="reuseConfigFrom"
|
||||
value={formData.reuseConfigFrom}
|
||||
onChange={handleChange}
|
||||
label={t('projects.reuseConfig')}
|
||||
>
|
||||
<MenuItem value="">{t('projects.noReuse')}</MenuItem>
|
||||
{projects.map(project => (
|
||||
<MenuItem key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
{error && (
|
||||
<Typography color="error" variant="body2" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={loading || !formData.name}
|
||||
sx={{
|
||||
background: theme.palette.gradient.primary,
|
||||
'&:hover': {
|
||||
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : t('home.createProject')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
135
easy-dataset-main/components/home/HeroSection.js
Normal file
135
easy-dataset-main/components/home/HeroSection.js
Normal file
@@ -0,0 +1,135 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Container, Typography, Button, useMediaQuery } from '@mui/material';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { styles } from '@/styles/home';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
import ParticleBackground from './ParticleBackground';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function HeroSection({ onCreateProject }) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
return (
|
||||
<Box sx={{ ...styles.heroSection, ...styles.heroBackground(theme) }}>
|
||||
{/* 添加粒子背景 */}
|
||||
<ParticleBackground />
|
||||
|
||||
<Box sx={styles.decorativeCircle} />
|
||||
<Box sx={styles.decorativeCircleSecond} />
|
||||
|
||||
<Container maxWidth="lg" sx={{ position: 'relative', zIndex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
maxWidth: '800px',
|
||||
mx: 'auto',
|
||||
py: { xs: 5, md: 8 }
|
||||
}}
|
||||
component={motion.div}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<Typography
|
||||
variant={isMobile ? 'h3' : 'h1'}
|
||||
component="h1"
|
||||
fontWeight="bold"
|
||||
sx={{
|
||||
...styles.gradientTitle(theme),
|
||||
letterSpacing: '-1px',
|
||||
mb: 3,
|
||||
textShadow: theme.palette.mode === 'dark' ? '0 0 30px rgba(139, 92, 246, 0.3)' : 'none'
|
||||
}}
|
||||
>
|
||||
{t('home.title')}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant={isMobile ? 'body1' : 'h5'}
|
||||
component={motion.p}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3, duration: 0.8 }}
|
||||
color="text.secondary"
|
||||
paragraph
|
||||
sx={{
|
||||
maxWidth: '650px',
|
||||
mx: 'auto',
|
||||
lineHeight: 1.8,
|
||||
opacity: 0.9,
|
||||
fontSize: { xs: '1rem', md: '1.2rem' },
|
||||
fontWeight: 400,
|
||||
mb: 4
|
||||
}}
|
||||
>
|
||||
{t('home.subtitle')}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
component={motion.div}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5, duration: 0.5 }}
|
||||
sx={{
|
||||
mt: 6,
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
justifyContent: 'center',
|
||||
gap: { xs: 2, sm: 3 }
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={onCreateProject}
|
||||
startIcon={<AddCircleOutlineIcon />}
|
||||
sx={{
|
||||
...styles.createButton(theme),
|
||||
fontWeight: 600,
|
||||
transition: 'all 0.3s ease',
|
||||
transform: 'translateY(0)',
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
borderRadius: '12px',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.2)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('home.createProject')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
window.location.href = '/dataset-square';
|
||||
}}
|
||||
startIcon={<SearchIcon />}
|
||||
sx={{
|
||||
...styles.createButton(theme),
|
||||
fontWeight: 600,
|
||||
transition: 'all 0.3s ease',
|
||||
transform: 'translateY(0)',
|
||||
px: 4,
|
||||
py: 1.5,
|
||||
borderRadius: '12px',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-3px)',
|
||||
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.2)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('home.searchDataset')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
300
easy-dataset-main/components/home/MigrationDialog.js
Normal file
300
easy-dataset-main/components/home/MigrationDialog.js
Normal file
@@ -0,0 +1,300 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
IconButton,
|
||||
Alert,
|
||||
Paper,
|
||||
useTheme,
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 项目迁移对话框组件
|
||||
* @param {Object} props - 组件属性
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Function} props.onClose - 关闭对话框的回调函数
|
||||
* @param {Array<string>} props.projectIds - 需要迁移的项目ID列表
|
||||
*/
|
||||
export default function MigrationDialog({ open, onClose, projectIds = [] }) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [migrating, setMigrating] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [migratedCount, setMigratedCount] = useState(0);
|
||||
const [taskId, setTaskId] = useState(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [statusText, setStatusText] = useState('');
|
||||
const [processingIds, setProcessingIds] = useState([]);
|
||||
|
||||
// 打开项目目录
|
||||
const handleOpenDirectory = async projectId => {
|
||||
try {
|
||||
setProcessingIds(prev => [...prev, projectId]);
|
||||
|
||||
const response = await fetch('/api/projects/open-directory', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ projectId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || t('migration.openDirectoryFailed'));
|
||||
}
|
||||
|
||||
// 成功打开目录,不需要特别处理
|
||||
} catch (err) {
|
||||
console.error('打开目录错误:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setProcessingIds(prev => prev.filter(id => id !== projectId));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除项目目录
|
||||
const handleDeleteDirectory = async projectId => {
|
||||
try {
|
||||
if (!window.confirm(t('migration.confirmDelete'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessingIds(prev => [...prev, projectId]);
|
||||
|
||||
const response = await fetch('/api/projects/delete-directory', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ projectId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || t('migration.deleteDirectoryFailed'));
|
||||
}
|
||||
|
||||
// 从列表中移除已删除的项目
|
||||
const updatedProjectIds = projectIds.filter(id => id !== projectId);
|
||||
// 这里我们不能直接修改 projectIds,因为它是从父组件传入的
|
||||
// 但我们可以通知用户界面刷新
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error('删除目录错误:', err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setProcessingIds(prev => prev.filter(id => id !== projectId));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理迁移操作
|
||||
const handleMigration = async () => {
|
||||
try {
|
||||
setMigrating(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
setProgress(0);
|
||||
setStatusText(t('migration.starting'));
|
||||
|
||||
// 调用异步迁移接口启动迁移任务
|
||||
const response = await fetch('/api/projects/migrate', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(t('migration.failed'));
|
||||
}
|
||||
|
||||
const { success, taskId: newTaskId } = await response.json();
|
||||
|
||||
if (!success || !newTaskId) {
|
||||
throw new Error(t('migration.startFailed'));
|
||||
}
|
||||
|
||||
// 保存任务ID
|
||||
setTaskId(newTaskId);
|
||||
setStatusText(t('migration.processing'));
|
||||
|
||||
// 开始轮询任务状态
|
||||
await pollMigrationStatus(newTaskId);
|
||||
} catch (err) {
|
||||
console.error('迁移错误:', err);
|
||||
setError(err.message);
|
||||
setMigrating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 轮询迁移任务状态
|
||||
const pollMigrationStatus = async id => {
|
||||
try {
|
||||
// 定义轮询间隔(毫秒)
|
||||
const pollInterval = 1000;
|
||||
|
||||
// 发送请求获取任务状态
|
||||
const response = await fetch(`/api/projects/migrate?taskId=${id}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(t('migration.statusFailed'));
|
||||
}
|
||||
|
||||
const { success, task } = await response.json();
|
||||
|
||||
if (!success || !task) {
|
||||
throw new Error(t('migration.taskNotFound'));
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
setProgress(task.progress || 0);
|
||||
|
||||
// 根据任务状态更新UI
|
||||
if (task.status === 'completed') {
|
||||
// 任务完成
|
||||
setMigratedCount(task.completed);
|
||||
setSuccess(true);
|
||||
setMigrating(false);
|
||||
setStatusText(t('migration.completed'));
|
||||
|
||||
// 迁移成功后,延迟关闭对话框并刷新页面
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else if (task.status === 'failed') {
|
||||
// 任务失败
|
||||
throw new Error(task.error || t('migration.failed'));
|
||||
} else {
|
||||
// 任务仍在进行中,继续轮询
|
||||
setTimeout(() => pollMigrationStatus(id), pollInterval);
|
||||
|
||||
// 更新状态文本
|
||||
if (task.total > 0) {
|
||||
setStatusText(
|
||||
t('migration.progressStatus', {
|
||||
completed: task.completed || 0,
|
||||
total: task.total
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取迁移状态错误:', err);
|
||||
setError(err.message);
|
||||
setMigrating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={migrating ? undefined : onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5
|
||||
}}
|
||||
>
|
||||
<WarningAmberIcon color="warning" />
|
||||
<Typography variant="h6">{t('migration.title')}</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{success ? (
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
{t('migration.success', { count: migratedCount })}
|
||||
</Alert>
|
||||
) : error ? (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||
{t('migration.description')}
|
||||
</Typography>
|
||||
|
||||
{projectIds.length > 0 && (
|
||||
<Box sx={{ mt: 2, mb: 2 }}>
|
||||
<Typography variant="subtitle1" sx={{ mb: 1 }}>
|
||||
{t('migration.projectsList')}:
|
||||
</Typography>
|
||||
<Paper variant="outlined" sx={{ maxHeight: 180, overflow: 'auto' }}>
|
||||
<List dense>
|
||||
{projectIds.map(id => (
|
||||
<ListItem key={id}>
|
||||
<ListItemText primary={id} />
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title={t('migration.openDirectory')}>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="open"
|
||||
onClick={() => handleOpenDirectory(id)}
|
||||
disabled={processingIds.includes(id)}
|
||||
size="small"
|
||||
>
|
||||
<FolderOpenIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('migration.deleteDirectory')}>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="delete"
|
||||
onClick={() => handleDeleteDirectory(id)}
|
||||
disabled={processingIds.includes(id)}
|
||||
size="small"
|
||||
sx={{ ml: 1, color: 'error.main' }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{migrating && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 3, gap: 1.5 }}>
|
||||
<CircularProgress variant={progress > 0 ? 'determinate' : 'indeterminate'} value={progress} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{statusText || t('migration.migrating')}
|
||||
</Typography>
|
||||
{progress > 0 && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{progress}%
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} disabled={migrating}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleMigration} variant="contained" color="primary" disabled={migrating || success}>
|
||||
{migrating ? t('migration.migrating') : t('migration.migrate')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
251
easy-dataset-main/components/home/ParticleBackground.js
Normal file
251
easy-dataset-main/components/home/ParticleBackground.js
Normal file
@@ -0,0 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTheme } from '@mui/material';
|
||||
|
||||
export default function ParticleBackground() {
|
||||
const canvasRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
let animationFrameId;
|
||||
let particles = [];
|
||||
let mousePosition = { x: 0, y: 0 };
|
||||
let hoverRadius = 150; // 增加鼠标影响范围
|
||||
let mouseSpeed = { x: 0, y: 0 }; // 跟踪鼠标速度
|
||||
let lastMousePosition = { x: 0, y: 0 }; // 上一帧鼠标位置
|
||||
|
||||
// 设置画布大小为窗口大小
|
||||
const handleResize = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
initParticles();
|
||||
};
|
||||
|
||||
// 跟踪鼠标位置和速度
|
||||
const handleMouseMove = event => {
|
||||
// 计算鼠标速度
|
||||
mouseSpeed.x = event.clientX - mousePosition.x;
|
||||
mouseSpeed.y = event.clientY - mousePosition.y;
|
||||
|
||||
// 更新鼠标位置
|
||||
lastMousePosition.x = mousePosition.x;
|
||||
lastMousePosition.y = mousePosition.y;
|
||||
mousePosition.x = event.clientX;
|
||||
mousePosition.y = event.clientY;
|
||||
};
|
||||
|
||||
// 触摸设备支持
|
||||
const handleTouchMove = event => {
|
||||
if (event.touches.length > 0) {
|
||||
// 计算触摸速度
|
||||
mouseSpeed.x = event.touches[0].clientX - mousePosition.x;
|
||||
mouseSpeed.y = event.touches[0].clientY - mousePosition.y;
|
||||
|
||||
// 更新触摸位置
|
||||
lastMousePosition.x = mousePosition.x;
|
||||
lastMousePosition.y = mousePosition.y;
|
||||
mousePosition.x = event.touches[0].clientX;
|
||||
mousePosition.y = event.touches[0].clientY;
|
||||
}
|
||||
};
|
||||
|
||||
// 生成随机颜色
|
||||
const getRandomColor = () => {
|
||||
// 主题色调
|
||||
const colors =
|
||||
theme.palette.mode === 'dark'
|
||||
? [
|
||||
'rgba(255, 255, 255, 0.5)', // 白色
|
||||
'rgba(100, 181, 246, 0.5)', // 蓝色
|
||||
'rgba(156, 39, 176, 0.4)', // 紫色
|
||||
'rgba(121, 134, 203, 0.5)' // 靛蓝色
|
||||
]
|
||||
: [
|
||||
'rgba(42, 92, 170, 0.5)', // 主蓝色
|
||||
'rgba(66, 165, 245, 0.4)', // 浅蓝色
|
||||
'rgba(94, 53, 177, 0.3)', // 深紫色
|
||||
'rgba(3, 169, 244, 0.4)' // 天蓝色
|
||||
];
|
||||
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
// 初始化粒子
|
||||
const initParticles = () => {
|
||||
particles = [];
|
||||
// 增加粒子数量,但保持性能平衡
|
||||
const particleCount = Math.min(Math.floor(window.innerWidth / 8), 150);
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
// 创建不同大小和速度的粒子
|
||||
const size = Math.random();
|
||||
const speedFactor = Math.max(0.1, size); // 较大的粒子移动较慢
|
||||
|
||||
particles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
// 粒子大小更加多样化
|
||||
radius: size * 3 + 0.5,
|
||||
// 使用随机颜色
|
||||
color: getRandomColor(),
|
||||
// 添加发光效果
|
||||
glow: Math.random() * 10 + 5,
|
||||
// 调整速度范围,使运动更加自然
|
||||
speedX: (Math.random() * 0.6 - 0.3) * speedFactor,
|
||||
speedY: (Math.random() * 0.6 - 0.3) * speedFactor,
|
||||
originalSpeedX: (Math.random() * 0.6 - 0.3) * speedFactor,
|
||||
originalSpeedY: (Math.random() * 0.6 - 0.3) * speedFactor,
|
||||
// 添加脉动效果
|
||||
pulseSpeed: Math.random() * 0.02 + 0.01,
|
||||
pulseDirection: Math.random() > 0.5 ? 1 : -1,
|
||||
pulseAmount: 0,
|
||||
// 粒子透明度
|
||||
opacity: Math.random() * 0.5 + 0.5
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 绘制粒子
|
||||
const drawParticles = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 计算鼠标速度衰减
|
||||
mouseSpeed.x *= 0.95;
|
||||
mouseSpeed.y *= 0.95;
|
||||
|
||||
// 绘制粒子之间的连线
|
||||
drawLines();
|
||||
|
||||
particles.forEach(particle => {
|
||||
// 计算粒子与鼠标的距离
|
||||
const dx = mousePosition.x - particle.x;
|
||||
const dy = mousePosition.y - particle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 脉动效果
|
||||
particle.pulseAmount += particle.pulseSpeed * particle.pulseDirection;
|
||||
if (Math.abs(particle.pulseAmount) > 0.5) {
|
||||
particle.pulseDirection *= -1;
|
||||
}
|
||||
|
||||
// 如果粒子在鼠标影响范围内,调整其速度
|
||||
if (distance < hoverRadius) {
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const force = (hoverRadius - distance) / hoverRadius;
|
||||
const mouseFactor = 3; // 增强鼠标影响力度
|
||||
|
||||
// 粒子远离鼠标,并受鼠标速度影响
|
||||
particle.speedX = -Math.cos(angle) * force * mouseFactor + particle.originalSpeedX + mouseSpeed.x * 0.05;
|
||||
particle.speedY = -Math.sin(angle) * force * mouseFactor + particle.originalSpeedY + mouseSpeed.y * 0.05;
|
||||
} else {
|
||||
// 逐渐恢复原始速度
|
||||
particle.speedX = particle.speedX * 0.95 + particle.originalSpeedX * 0.05;
|
||||
particle.speedY = particle.speedY * 0.95 + particle.originalSpeedY * 0.05;
|
||||
}
|
||||
|
||||
// 更新粒子位置
|
||||
particle.x += particle.speedX;
|
||||
particle.y += particle.speedY;
|
||||
|
||||
// 边界检查
|
||||
if (particle.x < 0) particle.x = canvas.width;
|
||||
if (particle.x > canvas.width) particle.x = 0;
|
||||
if (particle.y < 0) particle.y = canvas.height;
|
||||
if (particle.y > canvas.height) particle.y = 0;
|
||||
|
||||
// 应用脉动效果到粒子大小
|
||||
const currentRadius = particle.radius * (1 + particle.pulseAmount * 0.2);
|
||||
|
||||
// 绘制发光效果
|
||||
const gradient = ctx.createRadialGradient(particle.x, particle.y, 0, particle.x, particle.y, particle.glow);
|
||||
gradient.addColorStop(0, particle.color);
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
|
||||
// 绘制粒子
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, currentRadius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = particle.color;
|
||||
ctx.fill();
|
||||
|
||||
// 添加发光效果
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, particle.glow, 0, Math.PI * 2);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.globalAlpha = 0.3 * particle.opacity;
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1.0;
|
||||
});
|
||||
|
||||
animationFrameId = requestAnimationFrame(drawParticles);
|
||||
};
|
||||
|
||||
// 绘制粒子之间的连线
|
||||
const drawLines = () => {
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// 增加连线的最大距离
|
||||
const maxDistance = 120;
|
||||
|
||||
if (distance < maxDistance) {
|
||||
// 只在粒子距离小于maxDistance时绘制连线
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
|
||||
// 根据距离设置线条透明度
|
||||
const opacity = 1 - distance / maxDistance;
|
||||
|
||||
// 根据主题设置线条颜色
|
||||
const lineColor =
|
||||
theme.palette.mode === 'dark'
|
||||
? `rgba(255, 255, 255, ${opacity * 0.2})`
|
||||
: `rgba(42, 92, 170, ${opacity * 0.2})`;
|
||||
|
||||
ctx.strokeStyle = lineColor;
|
||||
ctx.lineWidth = opacity * 1.5; // 根据距离调整线宽
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('touchmove', handleTouchMove);
|
||||
|
||||
// 开始动画
|
||||
drawParticles();
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('touchmove', handleTouchMove);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, [theme.palette.mode]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
pointerEvents: 'none', // 确保不会干扰下方元素的交互
|
||||
zIndex: 0
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
117
easy-dataset-main/components/home/ProjectList.js
Normal file
117
easy-dataset-main/components/home/ProjectList.js
Normal file
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import ProjectCard from './ProjectCard';
|
||||
|
||||
export default function ProjectList({ projects, onCreateProject }) {
|
||||
const { t } = useTranslation();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [projectToDelete, setProjectToDelete] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// 打开删除确认对话框
|
||||
const handleOpenDeleteDialog = (event, project) => {
|
||||
setProjectToDelete(project);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// 关闭删除确认对话框
|
||||
const handleCloseDeleteDialog = () => {
|
||||
setDeleteDialogOpen(false);
|
||||
setProjectToDelete(null);
|
||||
};
|
||||
|
||||
// 删除项目
|
||||
const handleDeleteProject = async () => {
|
||||
if (!projectToDelete) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch(`/api/projects/${projectToDelete.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('projects.deleteFailed'));
|
||||
}
|
||||
|
||||
// 刷新页面以更新项目列表
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('删除项目失败:', error);
|
||||
alert(error.message || t('projects.deleteFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
handleCloseDeleteDialog();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container spacing={3}>
|
||||
{projects.length === 0 ? (
|
||||
<Grid item xs={12}>
|
||||
<Paper sx={{ p: 4, textAlign: 'center' }}>
|
||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||
{t('projects.noProjects')}
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={onCreateProject} startIcon={<AddCircleOutlineIcon />} sx={{ mt: 2 }}>
|
||||
{t('projects.createFirst')}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
) : (
|
||||
projects.map(project => (
|
||||
<Grid item xs={12} sm={6} md={4} key={project.id}>
|
||||
<ProjectCard project={project} onDeleteClick={handleOpenDeleteDialog} />
|
||||
</Grid>
|
||||
))
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<Dialog
|
||||
open={deleteDialogOpen}
|
||||
onClose={handleCloseDeleteDialog}
|
||||
aria-labelledby="delete-dialog-title"
|
||||
aria-describedby="delete-dialog-description"
|
||||
>
|
||||
<DialogTitle id="delete-dialog-title">{t('projects.deleteConfirmTitle')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="delete-dialog-description">
|
||||
{projectToDelete && (
|
||||
<>
|
||||
{t('projects.deleteConfirm')}
|
||||
<br />
|
||||
<Typography component="span" fontWeight="bold" sx={{ mt: 1, display: 'inline-block' }}>
|
||||
{projectToDelete.name}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseDeleteDialog} disabled={loading}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleDeleteProject} color="error" variant="contained" disabled={loading}>
|
||||
{loading ? t('common.deleting') : t('common.delete')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
118
easy-dataset-main/components/home/StatsCard.js
Normal file
118
easy-dataset-main/components/home/StatsCard.js
Normal file
@@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
import { Paper, Grid, Box, Typography, useMediaQuery, Avatar } from '@mui/material';
|
||||
import { styles } from '@/styles/home';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { motion } from 'framer-motion';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
|
||||
import StorageIcon from '@mui/icons-material/Storage';
|
||||
import MemoryIcon from '@mui/icons-material/Memory';
|
||||
|
||||
// 默认模型列表
|
||||
const mockModels = [
|
||||
{ id: 'deepseek-r1', provider: 'Ollama', name: 'DeepSeek-R1' },
|
||||
{ id: 'gpt-3.5-turbo-openai', provider: 'OpenAI', name: 'gpt-3.5-turbo' },
|
||||
{ id: 'gpt-3.5-turbo-guiji', provider: 'Guiji', name: 'gpt-3.5-turbo' },
|
||||
{ id: 'glm-4-flash', provider: 'Zhipu AI', name: 'GLM-4-Flash' }
|
||||
];
|
||||
|
||||
export default function StatsCard({ projects }) {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
// 统计卡片数据
|
||||
const statsItems = [
|
||||
{
|
||||
value: projects.length,
|
||||
label: t('stats.ongoingProjects'),
|
||||
color: 'primary',
|
||||
icon: <FolderOpenIcon />
|
||||
},
|
||||
{
|
||||
value: projects.reduce((sum, project) => sum + (project.questionsCount || 0), 0),
|
||||
label: t('stats.questionCount'),
|
||||
color: 'secondary',
|
||||
icon: <QuestionAnswerIcon />
|
||||
},
|
||||
{
|
||||
value: projects.reduce((sum, project) => sum + (project.datasetsCount || 0), 0),
|
||||
label: t('stats.generatedDatasets'),
|
||||
color: 'success',
|
||||
icon: <StorageIcon />
|
||||
},
|
||||
{
|
||||
value: mockModels.length,
|
||||
label: t('stats.supportedModels'),
|
||||
color: 'warning',
|
||||
icon: <MemoryIcon />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={styles.statsCard(theme)}
|
||||
component={motion.div}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
{statsItems.map((item, index) => (
|
||||
<Grid item xs={12} sm={6} md={3} key={index}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-5px)',
|
||||
boxShadow: `0 10px 20px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.05)'}`
|
||||
}
|
||||
}}
|
||||
component={motion.div}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
transition={{ type: 'spring', stiffness: 300 }}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
mb: 2,
|
||||
bgcolor: theme.palette[item.color].main,
|
||||
color: '#fff',
|
||||
boxShadow: `0 4px 12px ${theme.palette[item.color].main}40`
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</Avatar>
|
||||
<Typography
|
||||
color={item.color + '.main'}
|
||||
variant={isMobile ? 'h3' : 'h2'}
|
||||
fontWeight="bold"
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
background: `linear-gradient(135deg, ${theme.palette[item.color].main} 0%, ${theme.palette[item.color].light} 100%)`,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
textFillColor: 'transparent'
|
||||
}}
|
||||
>
|
||||
{item.value}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary" fontWeight="500" sx={{ opacity: 0.8 }}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user