feat(knowledge): add knowledge base detail page components and hooks

refactor(knowledge): restructure knowledge detail page with new components
feat(components): add FileUploadDialog for file upload functionality
feat(hooks): implement document management hooks for CRUD operations
This commit is contained in:
2025-10-14 15:42:40 +08:00
parent 34181cf025
commit 7384ae36d0
12 changed files with 1456 additions and 356 deletions

View File

@@ -0,0 +1,323 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Typography,
Paper,
Chip,
IconButton,
TextField,
InputAdornment,
Menu,
MenuItem,
Tooltip,
Button,
Stack,
} from '@mui/material';
import {
Search as SearchIcon,
MoreVert as MoreVertIcon,
InsertDriveFile as FileIcon,
PictureAsPdf as PdfIcon,
Description as DocIcon,
Image as ImageIcon,
VideoFile as VideoIcon,
AudioFile as AudioIcon,
Upload as UploadIcon,
Refresh as RefreshIcon,
Delete as DeleteIcon,
} from '@mui/icons-material';
import { DataGrid, type GridColDef, type GridRowSelectionModel } from '@mui/x-data-grid';
import { zhCN, enUS } from '@mui/x-data-grid/locales';
import type { IKnowledgeFile } from '@/interfaces/database/knowledge';
import { RUNNING_STATUS_KEYS, type RunningStatus } from '@/constants/knowledge';
import { LanguageAbbreviation } from '@/locales';
import dayjs from 'dayjs';
interface FileListComponentProps {
files: IKnowledgeFile[];
loading: boolean;
searchKeyword: string;
onSearchChange: (keyword: string) => void;
onReparse: (fileIds: string[]) => void;
onDelete: (fileIds: string[]) => void;
onUpload: () => void;
onRefresh: () => void;
rowSelectionModel: GridRowSelectionModel;
onRowSelectionModelChange: (model: GridRowSelectionModel) => void;
}
const getFileIcon = (type: string) => {
switch (type.toLowerCase()) {
case 'pdf': return <PdfIcon color="error" />;
case 'doc':
case 'docx': return <DocIcon color="primary" />;
case 'jpg':
case 'jpeg':
case 'png':
case 'gif': return <ImageIcon color="success" />;
case 'mp4':
case 'avi':
case 'mov': return <VideoIcon color="secondary" />;
case 'mp3':
case 'wav': return <AudioIcon color="warning" />;
default: return <FileIcon />;
}
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const getStatusChip = (status: string) => {
return <Chip label={status === '1' ? '启用' : '禁用'}
color={status === '1' ? 'success' : 'error'}
size="small" />;
};
const getRunStatusChip = (run: RunningStatus, progress: number) => {
const statusConfig = {
[RUNNING_STATUS_KEYS.UNSTART]: { label: '未开始', color: 'default' as const },
[RUNNING_STATUS_KEYS.RUNNING]: { label: `解析中 ${progress}%`, color: 'info' as const },
[RUNNING_STATUS_KEYS.CANCEL]: { label: '已取消', color: 'warning' as const },
[RUNNING_STATUS_KEYS.DONE]: { label: '完成', color: 'success' as const },
[RUNNING_STATUS_KEYS.FAIL]: { label: '失败', color: 'error' as const },
};
const config = statusConfig[run] || { label: '未知', color: 'default' as const };
return <Chip label={config.label} color={config.color} size="small" />;
};
const FileListComponent: React.FC<FileListComponentProps> = ({
files,
loading,
searchKeyword,
onSearchChange,
onReparse,
onDelete,
onUpload,
onRefresh,
rowSelectionModel,
onRowSelectionModelChange,
}) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selectedFileId, setSelectedFileId] = useState<string>('');
const { i18n } = useTranslation();
// 根据当前语言获取DataGrid的localeText
const getDataGridLocale = () => {
const currentLanguage = i18n.language;
return currentLanguage === LanguageAbbreviation.Zh ? zhCN : enUS;
};
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, fileId: string) => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
setSelectedFileId(fileId);
};
const handleMenuClose = () => {
setAnchorEl(null);
setSelectedFileId('');
};
const handleReparse = () => {
if (selectedFileId) {
onReparse([selectedFileId]);
}
handleMenuClose();
};
const handleDelete = () => {
if (selectedFileId) {
onDelete([selectedFileId]);
}
handleMenuClose();
};
// 过滤文件列表
const filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchKeyword.toLowerCase())
);
// DataGrid 列定义
const columns: GridColDef[] = [
{
field: 'name',
headerName: '文件名',
flex: 1,
minWidth: 100,
maxWidth: 300,
cellClassName: 'grid-center-cell',
renderCell: (params) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getFileIcon(params.row.type)}
<Tooltip title={params.value}>
<Typography variant="body2">{params.value}</Typography>
</Tooltip>
</Box>
),
},
{
field: 'type',
headerName: '类型',
width: 100,
renderCell: (params) => (
<Chip label={params.value.toUpperCase()} size="small" variant="outlined" />
),
},
{
field: 'size',
headerName: '大小',
width: 120,
renderCell: (params) => formatFileSize(params.value),
},
{
field: 'chunk_num',
headerName: '分块数',
width: 100,
type: 'number',
},
{
field: 'status',
headerName: '状态',
width: 120,
renderCell: (params) => getStatusChip(params.value),
},
{
field: 'run',
headerName: '解析状态',
width: 120,
renderCell: (params) => getRunStatusChip(params.value, params.row.progress),
},
{
field: 'create_time',
headerName: '上传时间',
width: 160,
valueFormatter: (value) => dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
},
{
field: 'actions',
headerName: '操作',
width: 80,
sortable: false,
renderCell: (params) => (
<IconButton
size="small"
onClick={(e) => handleMenuClick(e, params.row.id)}
>
<MoreVertIcon />
</IconButton>
),
},
];
return (
<Box>
{/* 文件操作栏 */}
<Paper sx={{ p: 2, mb: 2 }}>
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between">
<TextField
placeholder="搜索文件..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
sx={{ minWidth: 300 }}
size="small"
/>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
startIcon={<UploadIcon />}
onClick={onUpload}
>
</Button>
{rowSelectionModel.ids.size > 0 && (
<>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => onReparse(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
>
({rowSelectionModel.ids.size})
</Button>
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={() => onDelete(Array.from(rowSelectionModel.ids).map((id) => id.toString()))}
>
({rowSelectionModel.ids.size})
</Button>
</>
)}
<IconButton onClick={onRefresh}>
<RefreshIcon />
</IconButton>
</Stack>
</Stack>
</Paper>
{/* 文件列表 */}
<Paper sx={{ height: 600, width: '100%' }}>
<DataGrid
rows={filteredFiles}
columns={columns}
loading={loading}
checkboxSelection
disableRowSelectionOnClick
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={onRowSelectionModelChange}
pageSizeOptions={[10, 25, 50, 100]}
initialState={{
pagination: {
paginationModel: { page: 0, pageSize: 25 },
},
}}
localeText={getDataGridLocale().components.MuiDataGrid.defaultProps.localeText}
sx={{
'& .MuiDataGrid-cell:focus': {
outline: 'none',
},
'& .MuiDataGrid-row:hover': {
backgroundColor: 'action.hover',
},
}}
/>
</Paper>
{/* 右边菜单 */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleReparse}>
<Typography></Typography>
</MenuItem>
<MenuItem onClick={handleDelete}>
<Typography color="error"></Typography>
</MenuItem>
</Menu>
</Box>
);
};
export default FileListComponent;

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { SpeedDial, SpeedDialAction } from '@mui/material';
import {
Settings as SettingsIcon,
Search as TestIcon,
Settings as ConfigIcon,
} from '@mui/icons-material';
interface FloatingActionButtonsProps {
onTestClick: () => void;
onConfigClick: () => void;
}
const FloatingActionButtons: React.FC<FloatingActionButtonsProps> = ({
onTestClick,
onConfigClick,
}) => {
const actions = [
{
icon: <TestIcon />,
name: '检索测试',
onClick: onTestClick,
},
{
icon: <ConfigIcon />,
name: '配置设置',
onClick: onConfigClick,
},
];
return (
<SpeedDial
ariaLabel="知识库操作"
sx={{
position: 'fixed',
bottom: 128,
right: 64,
'& .MuiSpeedDial-fab': {
bgcolor: 'primary.main',
'&:hover': {
bgcolor: 'primary.dark',
},
},
}}
icon={<SettingsIcon />}
>
{actions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
slotProps={{
tooltip: {
title: action.name,
sx: {
bgcolor: 'primary.main',
color: 'white',
":hover": {
bgcolor: 'primary.dark',
},
},
},
}}
onClick={action.onClick}
/>
))}
</SpeedDial>
);
};
export default FloatingActionButtons;

View File

@@ -0,0 +1,72 @@
import React from 'react';
import {
Card,
CardContent,
Grid,
Typography,
Chip,
Stack,
} from '@mui/material';
import dayjs from 'dayjs';
import type { IKnowledge } from '@/interfaces/database/knowledge';
interface KnowledgeInfoCardProps {
knowledgeBase: IKnowledge;
}
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const KnowledgeInfoCard: React.FC<KnowledgeInfoCardProps> = ({ knowledgeBase }) => {
return (
<Card sx={{ mb: 3 }}>
<CardContent>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 8 }}>
<Typography variant="h4" gutterBottom>
{knowledgeBase.name}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
{knowledgeBase.description || '暂无描述'}
</Typography>
<Stack direction="row" spacing={2} flexWrap="wrap">
<Chip label={`${knowledgeBase.doc_num || 0} 个文件`} variant="outlined" />
<Chip label={`${knowledgeBase.chunk_num || 0} 个分块`} variant="outlined" />
<Chip label={`${knowledgeBase.token_num || 0} 个令牌`} variant="outlined" />
<Chip label={`大小: ${formatFileSize(knowledgeBase.size || 0)}`} variant="outlined" />
</Stack>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Stack spacing={1} alignItems="flex-end">
<Typography variant="body2" color="text.secondary">
: {dayjs(knowledgeBase.create_time).format('YYYY-MM-DD HH:mm:ss')}
</Typography>
<Typography variant="body2" color="text.secondary">
: {dayjs(knowledgeBase.update_time).format('YYYY-MM-DD HH:mm:ss')}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.language || 'English'}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.permission}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.embd_id}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.parser_id}
</Typography>
</Stack>
</Grid>
</Grid>
</CardContent>
</Card>
);
};
export default KnowledgeInfoCard;