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:
323
src/pages/knowledge/components/FileListComponent.tsx
Normal file
323
src/pages/knowledge/components/FileListComponent.tsx
Normal 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;
|
||||
70
src/pages/knowledge/components/FloatingActionButtons.tsx
Normal file
70
src/pages/knowledge/components/FloatingActionButtons.tsx
Normal 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;
|
||||
72
src/pages/knowledge/components/KnowledgeInfoCard.tsx
Normal file
72
src/pages/knowledge/components/KnowledgeInfoCard.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user