Files

323 lines
9.2 KiB
JavaScript
Raw Permalink Normal View History

2026-03-17 14:36:31 +08:00
'use client';
import { useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import {
Box,
Container,
Typography,
Grid,
Pagination,
CircularProgress,
Alert,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Snackbar
} from '@mui/material';
import { Masonry } from '@mui/lab';
import { useTranslation } from 'react-i18next';
import useEvalDatasets from './hooks/useEvalDatasets';
import useExportEvalDatasets from './hooks/useExportEvalDatasets';
import EvalToolbar from './components/EvalToolbar';
import EvalDatasetCard from './components/EvalDatasetCard';
import EvalDatasetList from './components/EvalDatasetList';
import ImportDialog from './components/ImportDialog';
import BuiltinDatasetDialog from './components/BuiltinDatasetDialog';
import ExportEvalDialog from './components/ExportEvalDialog';
export default function EvalDatasetsPage() {
const { projectId } = useParams();
const router = useRouter();
const { t } = useTranslation();
const {
items,
total,
stats,
totalPages,
loading,
searching,
error,
page,
setPage,
questionType,
setQuestionType,
tags,
setTags,
keyword,
setKeyword,
viewMode,
setViewMode,
selectedIds,
toggleSelect,
toggleSelectAll,
fetchData,
deleteItems
} = useEvalDatasets(projectId);
// 导出 Hook
const {
dialogOpen: exportDialogOpen,
openDialog: openExportDialog,
closeDialog: closeExportDialog,
exporting,
error: exportError,
format: exportFormat,
setFormat: setExportFormat,
questionTypes: exportQuestionTypes,
setQuestionTypes: setExportQuestionTypes,
selectedTags: exportSelectedTags,
setSelectedTags: setExportSelectedTags,
keyword: exportKeyword,
setKeyword: setExportKeyword,
previewTotal,
previewLoading,
availableTags: exportAvailableTags,
resetFilters: resetExportFilters,
handleExport
} = useExportEvalDatasets(projectId, stats);
// 删除确认对话框
const [deleteDialog, setDeleteDialog] = useState({ open: false, ids: [] });
// 导入对话框
const [importDialogOpen, setImportDialogOpen] = useState(false);
const [builtinImportOpen, setBuiltinImportOpen] = useState(false);
// Toast 提示
const [toast, setToast] = useState({ open: false, message: '', severity: 'success' });
// 处理导入成功
const handleImportSuccess = result => {
setToast({
open: true,
message: t('evalDatasets.import.successMessage', { count: result.total }),
severity: 'success'
});
fetchData(); // 刷新数据
};
// 处理删除
const handleDelete = async ids => {
setDeleteDialog({ open: true, ids: Array.isArray(ids) ? ids : [ids] });
};
const confirmDelete = async () => {
try {
await deleteItems(deleteDialog.ids);
setDeleteDialog({ open: false, ids: [] });
} catch (err) {
console.error('Delete failed:', err);
}
};
// 处理编辑
const handleEdit = item => {
router.push(`/projects/${projectId}/eval-datasets/${item.id}`);
};
// 处理查看
const handleView = item => {
router.push(`/projects/${projectId}/eval-datasets/${item.id}`);
};
return (
<Container maxWidth="xl" sx={{ py: 3 }}>
{/* 错误提示 */}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{/* 工具栏(包含统计筛选) */}
<EvalToolbar
keyword={keyword}
onKeywordChange={setKeyword}
viewMode={viewMode}
onViewModeChange={setViewMode}
selectedCount={selectedIds.length}
onDeleteSelected={() => handleDelete(selectedIds)}
stats={stats}
questionType={questionType}
onTypeChange={setQuestionType}
tags={tags}
onTagsChange={setTags}
onRefresh={fetchData}
loading={loading}
onImport={() => setImportDialogOpen(true)}
onBuiltinImport={() => setBuiltinImportOpen(true)}
onExport={openExportDialog}
/>
{/* 加载状态 */}
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress />
</Box>
)}
{/* 内容区域 */}
{!loading && (
<Box sx={{ position: 'relative', minHeight: searching ? 200 : 'auto' }}>
{/* 搜索加载遮罩 */}
{searching && (
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'rgba(255, 255, 255, 0.7)',
zIndex: 10,
borderRadius: 2
}}
>
<CircularProgress size={32} />
</Box>
)}
{viewMode === 'card' ? (
<Box>
<Masonry
columns={{ xs: 1, sm: 2, md: 3, lg: 4 }}
spacing={3}
sx={{ opacity: searching ? 0.5 : 1, transition: 'opacity 0.2s', width: 'auto' }}
>
{items.map(item => (
<EvalDatasetCard
key={item.id}
item={item}
selected={selectedIds.includes(item.id)}
onSelect={toggleSelect}
onEdit={handleEdit}
onDelete={handleDelete}
projectId={projectId}
/>
))}
</Masonry>
</Box>
) : (
<Box sx={{ opacity: searching ? 0.5 : 1, transition: 'opacity 0.2s' }}>
<EvalDatasetList
items={items}
selectedIds={selectedIds}
onSelect={toggleSelect}
onSelectAll={toggleSelectAll}
onEdit={handleEdit}
onDelete={handleDelete}
onView={handleView}
/>
</Box>
)}
{/* 空状态 */}
{items.length === 0 && (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
py: 8
}}
>
<Typography variant="h6" color="text.secondary" sx={{ mb: 1 }}>
{t('eval.noData')}
</Typography>
<Typography variant="body2" color="text.disabled">
{t('eval.noDataHint')}
</Typography>
</Box>
)}
{/* 分页 */}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
<Pagination
count={totalPages}
page={page}
onChange={(e, value) => setPage(value)}
color="primary"
showFirstButton
showLastButton
/>
</Box>
)}
</Box>
)}
{/* 删除确认对话框 */}
<Dialog open={deleteDialog.open} onClose={() => setDeleteDialog({ open: false, ids: [] })}>
<DialogTitle>{t('eval.deleteConfirmTitle')}</DialogTitle>
<DialogContent>
<Typography>{t('eval.deleteConfirmMessage', { count: deleteDialog.ids.length })}</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialog({ open: false, ids: [] })}>{t('common.cancel')}</Button>
<Button color="error" variant="contained" onClick={confirmDelete}>
{t('common.delete')}
</Button>
</DialogActions>
</Dialog>
{/* 导入对话框 */}
<ImportDialog
open={importDialogOpen}
onClose={() => setImportDialogOpen(false)}
projectId={projectId}
onSuccess={handleImportSuccess}
/>
{/* 内置数据集导入对话框 */}
<BuiltinDatasetDialog
open={builtinImportOpen}
onClose={() => setBuiltinImportOpen(false)}
projectId={projectId}
onSuccess={handleImportSuccess}
/>
{/* 导出对话框 */}
<ExportEvalDialog
open={exportDialogOpen}
onClose={closeExportDialog}
exporting={exporting}
error={exportError}
format={exportFormat}
setFormat={setExportFormat}
questionTypes={exportQuestionTypes}
setQuestionTypes={setExportQuestionTypes}
selectedTags={exportSelectedTags}
setSelectedTags={setExportSelectedTags}
keyword={exportKeyword}
setKeyword={setExportKeyword}
previewTotal={previewTotal}
previewLoading={previewLoading}
availableTags={exportAvailableTags}
resetFilters={resetExportFilters}
onExport={handleExport}
/>
{/* Toast 提示 */}
<Snackbar
open={toast.open}
autoHideDuration={4000}
onClose={() => setToast({ ...toast, open: false })}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert severity={toast.severity} onClose={() => setToast({ ...toast, open: false })}>
{toast.message}
</Alert>
</Snackbar>
</Container>
);
}