feat(knowledge): add knowledge base management with dialog system

- Implement knowledge base list, create, and detail pages
- Add dialog provider and components for confirmation and alerts
- Include knowledge card and grid view components
- Enhance header with user menu and logout functionality
- Implement knowledge operations hooks for CRUD operations
This commit is contained in:
2025-10-13 12:26:10 +08:00
parent d475a0e982
commit 5c937df5ed
18 changed files with 2151 additions and 184 deletions

View File

@@ -3,6 +3,7 @@ import { CssBaseline, ThemeProvider } from '@mui/material';
import { theme } from './theme';
import AppRoutes from './routes';
import SnackbarProvider from './components/Provider/SnackbarProvider';
import DialogProvider from './components/Provider/DialogProvider';
import AuthGuard from './components/AuthGuard';
import './locales';
@@ -16,11 +17,13 @@ function MaterialUIApp() {
<ThemeProvider theme={theme}>
<CssBaseline />
<SnackbarProvider>
<BrowserRouter>
<AuthGuard>
<AppRoutes />
</AuthGuard>
</BrowserRouter>
<DialogProvider>
<BrowserRouter>
<AuthGuard>
<AppRoutes />
</AuthGuard>
</BrowserRouter>
</DialogProvider>
</SnackbarProvider>
</ThemeProvider>
);

View File

@@ -1,9 +1,40 @@
import { Box, InputBase } from '@mui/material';
import React, { useState } from 'react';
import {
Box,
InputBase,
Menu,
MenuItem,
Avatar,
Typography,
Divider,
ListItemIcon,
ListItemText
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import LogoutIcon from '@mui/icons-material/Logout';
import PersonIcon from '@mui/icons-material/Person';
import LanguageSwitcher from '../LanguageSwitcher';
import { useAuth } from '@/hooks/login_hooks';
const Header = () => {
const { userInfo, logout } = useAuth();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleAvatarClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleLogout = () => {
logout();
handleClose();
};
return (
<Box
sx={{
@@ -26,7 +57,7 @@ const Header = () => {
>
RAG Dashboard
</Box>
<Box
sx={{
display: 'flex',
@@ -64,18 +95,107 @@ const Header = () => {
placeholder="Search queries, KB names..."
/>
</Box>
<LanguageSwitcher textColor="#333" />
<AccountCircleIcon
sx={{
color: '#666',
cursor: 'pointer',
fontSize: '2rem',
marginLeft: '12px',
}}
titleAccess="User Profile"
/>
{/* 用户头像和菜单 */}
<Box sx={{ position: 'relative' }}>
{userInfo?.avatar ? (
<Avatar
src={userInfo.avatar}
alt={userInfo.nickname || userInfo.email}
sx={{
width: 32,
height: 32,
cursor: 'pointer',
marginLeft: '12px',
'&:hover': {
opacity: 0.8,
},
}}
onClick={handleAvatarClick}
/>
) : (
<Box onClick={handleAvatarClick}>
<AccountCircleIcon
sx={{
color: '#666',
cursor: 'pointer',
fontSize: '2rem',
marginLeft: '12px',
'&:hover': {
color: '#333',
},
}}
titleAccess="User Profile"
/>
</Box>
)}
{/* 用户菜单 */}
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
onClick={handleClose}
PaperProps={{
elevation: 3,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.15))',
mt: 1.5,
minWidth: 200,
'& .MuiAvatar-root': {
width: 24,
height: 24,
ml: -0.5,
mr: 1,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
{/* 用户信息 */}
<Box sx={{ px: 2, py: 1.5, borderBottom: '1px solid #f0f0f0' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#333' }}>
{userInfo?.nickname || '用户'}
</Typography>
<Typography variant="body2" sx={{ color: '#666', fontSize: '0.75rem' }}>
{userInfo?.email}
</Typography>
</Box>
{/* 菜单项 */}
<MenuItem onClick={handleClose} sx={{ py: 1 }}>
<ListItemIcon>
<PersonIcon fontSize="small" />
</ListItemIcon>
<ListItemText></ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={handleLogout} sx={{ py: 1, color: '#d32f2f' }}>
<ListItemIcon>
<LogoutIcon fontSize="small" sx={{ color: '#d32f2f' }} />
</ListItemIcon>
<ListItemText>退</ListItemText>
</MenuItem>
</Menu>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,162 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
IconButton,
} from '@mui/material';
import {
Close as CloseIcon,
Info as InfoIcon,
CheckCircle as SuccessIcon,
Warning as WarningIcon,
Error as ErrorIcon,
Help as ConfirmIcon,
} from '@mui/icons-material';
import { type IDialogInstance } from '../../interfaces/common';
interface DialogComponentProps {
dialog: IDialogInstance;
onClose: (result: boolean) => void;
}
const DialogComponent: React.FC<DialogComponentProps> = ({ dialog, onClose }) => {
const [loading, setLoading] = useState(false);
const { config } = dialog;
// 获取对话框图标
const getDialogIcon = () => {
const iconProps = { sx: { fontSize: 24, mr: 1 } };
switch (config.type) {
case 'info':
return <InfoIcon {...iconProps} color="info" />;
case 'success':
return <SuccessIcon {...iconProps} color="success" />;
case 'warning':
return <WarningIcon {...iconProps} color="warning" />;
case 'error':
return <ErrorIcon {...iconProps} color="error" />;
case 'confirm':
return <ConfirmIcon {...iconProps} color="warning" />;
default:
return null;
}
};
// 获取确认按钮颜色
const getConfirmButtonColor = () => {
switch (config.type) {
case 'error':
case 'warning':
return 'error';
case 'success':
return 'success';
case 'info':
return 'info';
default:
return 'primary';
}
};
// 处理确认操作
const handleConfirm = async () => {
try {
setLoading(true);
if (config.onConfirm) {
await config.onConfirm();
}
onClose(true);
} catch (error) {
console.error('Dialog confirm error:', error);
// 即使出错也关闭对话框但返回false
onClose(false);
} finally {
setLoading(false);
}
};
// 处理取消操作
const handleCancel = () => {
if (config.onCancel) {
config.onCancel();
}
onClose(false);
};
// 处理遮罩点击
const handleBackdropClick = () => {
if (config.maskClosable !== false) {
handleCancel();
}
};
return (
<Dialog
open={true}
onClose={handleBackdropClick}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
width: config.width || 'auto',
maxWidth: config.width || '500px',
},
}}
>
{/* 标题栏 */}
<DialogTitle sx={{ display: 'flex', alignItems: 'center', pr: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
{getDialogIcon()}
<Typography variant="h6" component="span">
{config.title || '提示'}
</Typography>
</Box>
<IconButton
onClick={handleCancel}
size="small"
sx={{ ml: 1 }}
>
<CloseIcon />
</IconButton>
</DialogTitle>
{/* 内容区域 */}
<DialogContent>
<Typography variant="body1" sx={{ py: 1 }}>
{config.content}
</Typography>
</DialogContent>
{/* 操作按钮 */}
<DialogActions sx={{ px: 3, pb: 2 }}>
{config.showCancel !== false && (
<Button
onClick={handleCancel}
variant="outlined"
disabled={loading}
>
{config.cancelText || '取消'}
</Button>
)}
<Button
onClick={handleConfirm}
variant="contained"
color={getConfirmButtonColor() as any}
disabled={loading}
sx={{ ml: 1 }}
>
{loading ? '处理中...' : (config.confirmText || '确定')}
</Button>
</DialogActions>
</Dialog>
);
};
export default DialogComponent;

View File

@@ -0,0 +1,117 @@
import React, { createContext, useContext, useState, useCallback, type PropsWithChildren } from 'react';
import type { IDialogConfig, IDialogInstance, IDialogContextValue } from '../../interfaces/common';
import DialogComponent from './DialogComponent';
// 创建Dialog上下文
export const DialogContext = createContext<IDialogContextValue | null>(null);
// 生成唯一ID的工具函数
const generateId = () => `dialog_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
export const DialogProvider: React.FC<PropsWithChildren> = ({ children }) => {
const [dialogs, setDialogs] = useState<IDialogInstance[]>([]);
// 打开对话框的通用方法
const openDialog = useCallback((config: IDialogConfig): Promise<boolean> => {
return new Promise((resolve, reject) => {
const id = generateId();
const dialogInstance: IDialogInstance = {
id,
config,
resolve,
reject,
};
setDialogs(prev => [...prev, dialogInstance]);
});
}, []);
// 关闭对话框
const closeDialog = useCallback((id: string, result: boolean = false) => {
setDialogs(prev => {
const dialog = prev.find(d => d.id === id);
if (dialog) {
dialog.resolve(result);
}
return prev.filter(d => d.id !== id);
});
}, []);
// 确认对话框
const confirm = useCallback((config: Omit<IDialogConfig, 'type'>): Promise<boolean> => {
return openDialog({
...config,
type: 'confirm',
showCancel: true,
confirmText: config.confirmText || '确定',
cancelText: config.cancelText || '取消',
});
}, [openDialog]);
// 信息对话框
const info = useCallback((config: Omit<IDialogConfig, 'type'>): Promise<boolean> => {
return openDialog({
...config,
type: 'info',
showCancel: false,
confirmText: config.confirmText || '确定',
});
}, [openDialog]);
// 成功对话框
const success = useCallback((config: Omit<IDialogConfig, 'type'>): Promise<boolean> => {
return openDialog({
...config,
type: 'success',
showCancel: false,
confirmText: config.confirmText || '确定',
});
}, [openDialog]);
// 警告对话框
const warning = useCallback((config: Omit<IDialogConfig, 'type'>): Promise<boolean> => {
return openDialog({
...config,
type: 'warning',
showCancel: false,
confirmText: config.confirmText || '确定',
});
}, [openDialog]);
// 错误对话框
const error = useCallback((config: Omit<IDialogConfig, 'type'>): Promise<boolean> => {
return openDialog({
...config,
type: 'error',
showCancel: false,
confirmText: config.confirmText || '确定',
});
}, [openDialog]);
const contextValue: IDialogContextValue = {
dialogs,
openDialog,
closeDialog,
confirm,
info,
success,
warning,
error,
};
return (
<DialogContext.Provider value={contextValue}>
{children}
{/* 渲染所有对话框 */}
{dialogs.map(dialog => (
<DialogComponent
key={dialog.id}
dialog={dialog}
onClose={(result) => closeDialog(dialog.id, result)}
/>
))}
</DialogContext.Provider>
);
};
export default DialogProvider;

View File

@@ -20,27 +20,13 @@ import {
} from '@mui/icons-material';
import type { IKnowledge } from '@/interfaces/database/knowledge';
interface KnowledgeGridViewProps {
knowledgeBases: IKnowledge[];
maxItems?: number;
showSeeAll?: boolean;
onSeeAll?: () => void;
onEdit?: (kb: IKnowledge) => void;
onDelete?: (kb: IKnowledge) => void;
onView?: (kb: IKnowledge) => void;
loading?: boolean;
// 新增属性用于控制空状态显示
searchTerm?: string;
teamFilter?: string;
onCreateKnowledge?: () => void;
}
interface KnowledgeCardProps {
knowledge: IKnowledge;
onMenuClick: (event: React.MouseEvent<HTMLElement>, kb: IKnowledge) => void;
onViewKnowledge: (kb: IKnowledge) => void;
}
const KnowledgeCard: React.FC<KnowledgeCardProps> = ({ knowledge, onMenuClick }) => {
const KnowledgeCard: React.FC<KnowledgeCardProps> = ({ knowledge, onMenuClick, onViewKnowledge }) => {
const getStatusInfo = (permission: string) => {
switch (permission) {
case 'me':
@@ -86,13 +72,13 @@ const KnowledgeCard: React.FC<KnowledgeCardProps> = ({ knowledge, onMenuClick })
},
}}
>
<CardContent>
<CardContent onClick={() => onViewKnowledge(knowledge)}>
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
<Box display="flex" alignItems="center" gap={1}>
{/* 显示avatar */}
{knowledge.avatar ? (
<Avatar
src={knowledge.avatar}
<Avatar
src={knowledge.avatar}
sx={{ width: 32, height: 32 }}
/>
) : (
@@ -115,7 +101,10 @@ const KnowledgeCard: React.FC<KnowledgeCardProps> = ({ knowledge, onMenuClick })
/>
<IconButton
size="small"
onClick={(e) => onMenuClick(e, knowledge)}
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
onMenuClick(e, knowledge);
}}
>
<MoreVertIcon />
</IconButton>
@@ -125,8 +114,8 @@ const KnowledgeCard: React.FC<KnowledgeCardProps> = ({ knowledge, onMenuClick })
<Typography
variant="body2"
color="text.secondary"
sx={{
mt: 1,
sx={{
mt: 1,
mb: 2,
display: '-webkit-box',
WebkitLineClamp: 2,
@@ -186,7 +175,7 @@ const KnowledgeCard: React.FC<KnowledgeCardProps> = ({ knowledge, onMenuClick })
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
: {formatUpdateTime(knowledge.update_time)}
</Typography>
{/* 显示创建者 */}
{knowledge.nickname && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
@@ -205,127 +194,4 @@ const KnowledgeCard: React.FC<KnowledgeCardProps> = ({ knowledge, onMenuClick })
);
};
const KnowledgeGridView: React.FC<KnowledgeGridViewProps> = ({
knowledgeBases,
maxItems,
showSeeAll = false,
onSeeAll,
onEdit,
onDelete,
onView,
loading = false,
searchTerm = '',
teamFilter = 'all',
onCreateKnowledge,
}) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [selectedKB, setSelectedKB] = React.useState<IKnowledge | null>(null);
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, kb: IKnowledge) => {
setAnchorEl(event.currentTarget);
setSelectedKB(kb);
};
const handleMenuClose = () => {
setAnchorEl(null);
setSelectedKB(null);
};
const handleEdit = () => {
if (selectedKB && onEdit) {
onEdit(selectedKB);
}
handleMenuClose();
};
const handleDelete = () => {
if (selectedKB && onDelete) {
onDelete(selectedKB);
}
handleMenuClose();
};
const handleView = () => {
if (selectedKB && onView) {
onView(selectedKB);
}
handleMenuClose();
};
const displayedKBs = maxItems ? knowledgeBases.slice(0, maxItems) : knowledgeBases;
const hasMore = maxItems && knowledgeBases.length > maxItems;
if (loading) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography>...</Typography>
</Box>
);
}
if (knowledgeBases.length === 0) {
return (
<Box sx={{ textAlign: 'center', py: 8 }}>
<FolderIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
{searchTerm || teamFilter !== 'all' ? '没有找到匹配的知识库' : '暂无知识库'}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{searchTerm || teamFilter !== 'all'
? '尝试调整搜索条件或筛选器'
: '创建您的第一个知识库开始使用'
}
</Typography>
{(!searchTerm && teamFilter === 'all' && onCreateKnowledge) && (
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={onCreateKnowledge}
>
</Button>
)}
</Box>
);
}
return (
<Box>
<Grid container spacing={3}>
{displayedKBs.map((kb) => (
<Grid key={kb.id} size={{ xs: 12, sm: 6, md: 4 }}>
<KnowledgeCard knowledge={kb} onMenuClick={handleMenuClick} />
</Grid>
))}
</Grid>
{showSeeAll && hasMore && (
<Box sx={{ textAlign: 'center', mt: 3 }}>
<Button
variant="outlined"
endIcon={<ArrowForwardIcon />}
onClick={onSeeAll}
sx={{ borderRadius: 2 }}
>
({knowledgeBases.length})
</Button>
</Box>
)}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleView}></MenuItem>
<MenuItem onClick={handleEdit}></MenuItem>
<MenuItem onClick={handleMenuClose}></MenuItem>
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
</MenuItem>
</Menu>
</Box>
);
};
export default KnowledgeGridView;
export default KnowledgeCard;

View File

@@ -0,0 +1,161 @@
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
Grid,
Chip,
IconButton,
Menu,
MenuItem,
Button,
Avatar,
} from '@mui/material';
import {
MoreVert as MoreVertIcon,
Folder as FolderIcon,
ArrowForward as ArrowForwardIcon,
Add as AddIcon,
} from '@mui/icons-material';
import type { IKnowledge } from '@/interfaces/database/knowledge';
import KnowledgeCard from './KnowledgeCard';
interface KnowledgeGridViewProps {
knowledgeBases: IKnowledge[];
maxItems?: number;
showSeeAll?: boolean;
onSeeAll?: () => void;
onEdit?: (kb: IKnowledge) => void;
onDelete?: (kb: IKnowledge) => void;
onView?: (kb: IKnowledge) => void;
loading?: boolean;
// 新增属性用于控制空状态显示
searchTerm?: string;
teamFilter?: string;
onCreateKnowledge?: () => void;
}
const KnowledgeGridView: React.FC<KnowledgeGridViewProps> = ({
knowledgeBases,
maxItems,
showSeeAll = false,
onSeeAll,
onEdit,
onDelete,
onView,
loading = false,
searchTerm = '',
teamFilter = 'all',
onCreateKnowledge,
}) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [selectedKB, setSelectedKB] = React.useState<IKnowledge | null>(null);
const handleMenuClick = (event: React.MouseEvent<HTMLElement>, kb: IKnowledge) => {
setAnchorEl(event.currentTarget);
setSelectedKB(kb);
};
const handleMenuClose = () => {
setAnchorEl(null);
setSelectedKB(null);
};
const handleDelete = () => {
if (selectedKB && onDelete) {
onDelete(selectedKB);
}
handleMenuClose();
};
const handleView = () => {
if (selectedKB && onView) {
onView(selectedKB);
}
handleMenuClose();
}
const handleViewKnowledge = (kb: IKnowledge) => {
if (onView) {
onView(kb);
}
handleMenuClose();
};
const displayedKBs = maxItems ? knowledgeBases.slice(0, maxItems) : knowledgeBases;
const hasMore = maxItems && knowledgeBases.length > maxItems;
if (loading) {
return (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography>...</Typography>
</Box>
);
}
if (knowledgeBases.length === 0) {
return (
<Box sx={{ textAlign: 'center', py: 8 }}>
<FolderIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
{searchTerm || teamFilter !== 'all' ? '没有找到匹配的知识库' : '暂无知识库'}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{searchTerm || teamFilter !== 'all'
? '尝试调整搜索条件或筛选器'
: '创建您的第一个知识库开始使用'
}
</Typography>
{(!searchTerm && teamFilter === 'all' && onCreateKnowledge) && (
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={onCreateKnowledge}
>
</Button>
)}
</Box>
);
}
return (
<Box>
<Grid container spacing={3}>
{displayedKBs.map((kb) => (
<Grid key={kb.id} size={{ xs: 12, sm: 6, md: 4 }}>
<KnowledgeCard knowledge={kb} onMenuClick={handleMenuClick} onViewKnowledge={handleViewKnowledge} />
</Grid>
))}
</Grid>
{showSeeAll && hasMore && (
<Box sx={{ textAlign: 'center', mt: 3 }}>
<Button
variant="outlined"
endIcon={<ArrowForwardIcon />}
onClick={onSeeAll}
sx={{ borderRadius: 2 }}
>
({knowledgeBases.length})
</Button>
</Box>
)}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
>
<MenuItem onClick={handleView}></MenuItem>
<MenuItem onClick={handleDelete} sx={{ color: 'error.main' }}>
</MenuItem>
</Menu>
</Box>
);
};
export default KnowledgeGridView;

View File

@@ -0,0 +1,193 @@
import React from 'react';
import { Box, Button, Typography, Stack } from '@mui/material';
import { useDialog } from '../hooks/useDialog';
const DialogExample: React.FC = () => {
const dialog = useDialog();
// 确认对话框示例
const handleConfirmDialog = async () => {
try {
const confirmed = await dialog.confirm({
title: '确认删除',
content: `确定要删除用户 "张三" 吗?此操作不可恢复。`,
confirmText: '删除',
cancelText: '取消',
onConfirm: async () => {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('用户已删除');
}
});
if (confirmed) {
console.log('用户确认删除');
} else {
console.log('用户取消删除');
}
} catch (error) {
console.error('删除操作失败:', error);
}
};
// 信息对话框示例
const handleInfoDialog = async () => {
await dialog.info({
title: '系统信息',
content: '这是一个信息提示对话框,用于显示重要信息。',
confirmText: '我知道了'
});
};
// 成功对话框示例
const handleSuccessDialog = async () => {
await dialog.success({
title: '操作成功',
content: '您的操作已成功完成!',
confirmText: '好的'
});
};
// 警告对话框示例
const handleWarningDialog = async () => {
await dialog.warning({
title: '警告',
content: '请注意:此操作可能会影响系统性能,建议在非高峰期执行。',
confirmText: '了解'
});
};
// 错误对话框示例
const handleErrorDialog = async () => {
await dialog.error({
title: '操作失败',
content: '抱歉,操作执行失败。请检查网络连接后重试。',
confirmText: '重试'
});
};
// 自定义对话框示例
const handleCustomDialog = async () => {
const result = await dialog.openDialog({
title: '自定义对话框',
content: (
<Box>
<Typography variant="body1" gutterBottom>
</Typography>
<Typography variant="body2" color="text.secondary">
React组件
</Typography>
</Box>
),
type: 'confirm',
confirmText: '同意',
cancelText: '拒绝',
width: 600,
maskClosable: false,
onConfirm: async () => {
await new Promise(resolve => setTimeout(resolve, 500));
console.log('用户同意了自定义操作');
}
});
console.log('自定义对话框结果:', result);
};
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Dialog 使
</Typography>
<Typography variant="body1" sx={{ mb: 3 }}>
使
</Typography>
<Stack spacing={2} direction="row" flexWrap="wrap" gap={2}>
<Button
variant="contained"
color="error"
onClick={handleConfirmDialog}
>
</Button>
<Button
variant="contained"
color="info"
onClick={handleInfoDialog}
>
</Button>
<Button
variant="contained"
color="success"
onClick={handleSuccessDialog}
>
</Button>
<Button
variant="contained"
color="warning"
onClick={handleWarningDialog}
>
</Button>
<Button
variant="contained"
color="error"
onClick={handleErrorDialog}
>
</Button>
<Button
variant="outlined"
onClick={handleCustomDialog}
>
</Button>
</Stack>
<Box sx={{ mt: 4 }}>
<Typography variant="h6" gutterBottom>
使
</Typography>
<Typography variant="body2" component="pre" sx={{
backgroundColor: 'grey.100',
p: 2,
borderRadius: 1,
fontFamily: 'monospace',
whiteSpace: 'pre-wrap'
}}>
{`const dialog = useDialog();
// 确认对话框
const confirmed = await dialog.confirm({
title: '确认删除',
content: '确定要删除用户 "张三" 吗?此操作不可恢复。',
confirmText: '删除',
cancelText: '取消',
});
// 信息对话框
await dialog.info({
title: '系统信息',
content: '这是一个信息提示。',
});
// 其他类型
await dialog.success({ ... });
await dialog.warning({ ... });
await dialog.error({ ... });`}
</Typography>
</Box>
</Box>
);
};
export default DialogExample;

View File

@@ -185,4 +185,212 @@ export const useKnowledgeDetail = (kbId: string) => {
error,
refresh: fetchKnowledgeDetail,
};
};
};
/**
* 知识库操作Hook
* 提供创建、更新、删除知识库的功能
*/
export const useKnowledgeOperations = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* 创建知识库
*/
const createKnowledge = useCallback(async (data: Partial<IKnowledge>) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.createKnowledge(data);
if (response.data.code === 0) {
return response.data.data;
} else {
throw new Error(response.data.message || '创建知识库失败');
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || '创建知识库失败';
setError(errorMessage);
console.error('Failed to create knowledge:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 更新知识库基础信息
* 包括名称、描述、语言等基本信息
*/
const updateKnowledgeBasicInfo = useCallback(async (data: {
id: string;
name?: string;
description?: string;
language?: string;
avatar?: any;
permission?: string;
}) => {
try {
setLoading(true);
setError(null);
const updateData = {
kb_id: data.id,
...data,
};
const response = await knowledgeService.updateKnowledge(updateData);
if (response.data.code === 0) {
return response.data.data;
} else {
throw new Error(response.data.message || '更新知识库基础信息失败');
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || '更新知识库基础信息失败';
setError(errorMessage);
console.error('Failed to update knowledge basic info:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 更新知识库模型配置
* 包括嵌入模型、解析器配置、相似度阈值等
*/
const updateKnowledgeModelConfig = useCallback(async (data: {
id: string;
embd_id?: string;
// parser_config?: Partial<ParserConfig>;
similarity_threshold?: number;
vector_similarity_weight?: number;
parser_id?: string;
}) => {
try {
setLoading(true);
setError(null);
const updateData = {
kb_id: data.id,
...data,
};
const response = await knowledgeService.updateKnowledge(updateData);
if (response.data.code === 0) {
return response.data.data;
} else {
throw new Error(response.data.message || '更新知识库模型配置失败');
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || '更新知识库模型配置失败';
setError(errorMessage);
console.error('Failed to update knowledge model config:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 删除知识库
*/
const deleteKnowledge = useCallback(async (kbId: string) => {
try {
setLoading(true);
setError(null);
const response = await knowledgeService.removeKnowledge({ kb_id: kbId });
if (response.data.code === 0) {
return response.data.data;
} else {
throw new Error(response.data.message || '删除知识库失败');
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || '删除知识库失败';
setError(errorMessage);
console.error('Failed to delete knowledge:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 清除错误状态
*/
const clearError = useCallback(() => {
setError(null);
}, []);
return {
loading,
error,
createKnowledge,
updateKnowledgeBasicInfo,
updateKnowledgeModelConfig,
deleteKnowledge,
clearError,
};
};
/**
* 知识库批量操作Hook
* 提供批量删除等功能
*/
export const useKnowledgeBatchOperations = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
/**
* 批量删除知识库
*/
const batchDeleteKnowledge = useCallback(async (kbIds: string[]) => {
try {
setLoading(true);
setError(null);
const results = await Promise.allSettled(
kbIds.map(kbId => knowledgeService.removeKnowledge({ kb_id: kbId }))
);
const failures = results
.map((result, index) => ({ result, index }))
.filter(({ result }) => result.status === 'rejected')
.map(({ index }) => kbIds[index]);
if (failures.length > 0) {
throw new Error(`删除失败的知识库: ${failures.join(', ')}`);
}
return results;
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || '批量删除知识库失败';
setError(errorMessage);
console.error('Failed to batch delete knowledge:', err);
throw err;
} finally {
setLoading(false);
}
}, []);
/**
* 清除错误状态
*/
const clearError = useCallback(() => {
setError(null);
}, []);
return {
loading,
error,
batchDeleteKnowledge,
clearError,
};
};

View File

@@ -99,8 +99,10 @@ export const useAuth = () => {
// 登出功能
const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem('Authorization');
localStorage.removeItem('userInfo');
setToken(null);
setAuthorization(null);
setUserInfo(null);
navigate('/login');
};

12
src/hooks/useDialog.ts Normal file
View File

@@ -0,0 +1,12 @@
import { useContext } from 'react';
import { DialogContext } from '../components/Provider/DialogProvider';
import { type IDialogContextValue } from '../interfaces/common';
// 导出useDialog hook
export const useDialog = (): IDialogContextValue => {
const context = useContext(DialogContext);
if (!context) {
throw new Error('useDialog must be used within a DialogProvider');
}
return context;
};

View File

@@ -1,3 +1,5 @@
import React from 'react';
export interface Pagination {
current: number;
pageSize: number;
@@ -23,3 +25,35 @@ export interface ResponseType {
message?: string;
data?: any;
}
// Dialog相关接口定义
export interface IDialogConfig {
title?: string;
content?: React.ReactNode;
type?: 'info' | 'success' | 'warning' | 'error' | 'confirm';
confirmText?: string;
cancelText?: string;
showCancel?: boolean;
maskClosable?: boolean;
width?: number | string;
onConfirm?: () => void | Promise<void>;
onCancel?: () => void;
}
export interface IDialogInstance {
id: string;
config: IDialogConfig;
resolve: (value: boolean) => void;
reject: (reason?: any) => void;
}
export interface IDialogContextValue {
dialogs: IDialogInstance[];
openDialog: (config: IDialogConfig) => Promise<boolean>;
closeDialog: (id: string, result?: boolean) => void;
confirm: (config: Omit<IDialogConfig, 'type'>) => Promise<boolean>;
info: (config: Omit<IDialogConfig, 'type'>) => Promise<boolean>;
success: (config: Omit<IDialogConfig, 'type'>) => Promise<boolean>;
warning: (config: Omit<IDialogConfig, 'type'>) => Promise<boolean>;
error: (config: Omit<IDialogConfig, 'type'>) => Promise<boolean>;
}

View File

@@ -29,7 +29,7 @@ import {
CheckCircle as CheckCircleIcon,
} from '@mui/icons-material';
import { styled } from '@mui/material/styles';
import KnowledgeGridView from '@/components/KnowledgeGridView';
import KnowledgeGridView from '@/components/knowledge/KnowledgeGridView';
import UserDataDebug from '@/components/UserDataDebug';
import { useNavigate } from 'react-router-dom';

View File

@@ -0,0 +1,503 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import {
Box,
Typography,
Card,
CardContent,
TextField,
Button,
FormControl,
InputLabel,
Select,
MenuItem,
Alert,
Grid,
Divider,
CircularProgress,
Stepper,
Step,
StepLabel,
Switch,
FormControlLabel,
Slider,
Chip,
Stack,
} from '@mui/material';
import {
ArrowBack as ArrowBackIcon,
Save as SaveIcon,
Settings as SettingsIcon,
CheckCircle as CheckCircleIcon,
SkipNext as SkipNextIcon,
} from '@mui/icons-material';
import knowledgeService from '@/services/knowledge_service';
// 基础信息表单数据
interface BasicFormData {
name: string;
description: string;
permission: string;
avatar?: string;
}
// 配置设置表单数据
interface ConfigFormData {
parser_id: string;
embd_id: string;
chunk_token_num: number;
layout_recognize: string;
delimiter: string;
auto_keywords: number;
auto_questions: number;
html4excel: boolean;
topn_tags: number;
use_raptor: boolean;
use_graphrag: boolean;
graphrag_method: string;
pagerank: number;
}
const steps = ['基础信息', '配置设置'];
function KnowledgeBaseCreate() {
const navigate = useNavigate();
const [activeStep, setActiveStep] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
const [createdKbId, setCreatedKbId] = useState<string>('');
// 基础信息表单
const basicForm = useForm<BasicFormData>({
defaultValues: {
name: '',
description: '',
permission: 'me',
avatar: undefined,
},
});
// 配置设置表单
const configForm = useForm<ConfigFormData>({
defaultValues: {
parser_id: 'naive',
embd_id: 'text-embedding-v3@Tongyi-Qianwen',
chunk_token_num: 512,
layout_recognize: 'DeepDOC',
delimiter: '\n',
auto_keywords: 0,
auto_questions: 0,
html4excel: false,
topn_tags: 3,
use_raptor: false,
use_graphrag: false,
graphrag_method: 'light',
pagerank: 0,
},
});
// 第一步:创建基础知识库
const handleBasicSubmit = async (data: BasicFormData) => {
setIsSubmitting(true);
setError('');
setSuccess('');
try {
// 只发送基础字段到 create API
const basicData = {
name: data.name,
avatar: data.avatar,
description: data.description,
permission: data.permission,
};
const response = await knowledgeService.createKnowledge(basicData);
// 假设 API 返回包含 kb_id 的响应
const kbId = response.data?.kb_id;
setCreatedKbId(kbId);
setSuccess('知识库创建成功!您可以继续配置解析设置,或直接跳过。');
setActiveStep(1); // 进入第二步
} catch (err: any) {
console.error('创建知识库失败:', err);
setError(err.response?.data?.message || err.message || '创建知识库失败,请重试');
} finally {
setIsSubmitting(false);
}
};
// 第二步:更新配置设置
const handleConfigSubmit = async (data: ConfigFormData) => {
if (!createdKbId) {
setError('未找到知识库ID请重新创建');
return;
}
setIsSubmitting(true);
setError('');
try {
// 构建 update API 的数据结构
const updateData:any = {
kb_id: createdKbId,
name: basicForm.getValues('name'),
description: basicForm.getValues('description'),
permission: basicForm.getValues('permission'),
parser_id: data.parser_id,
embd_id: data.embd_id,
parser_config: {
layout_recognize: data.layout_recognize,
chunk_token_num: data.chunk_token_num,
delimiter: data.delimiter,
auto_keywords: data.auto_keywords,
auto_questions: data.auto_questions,
html4excel: data.html4excel,
topn_tags: data.topn_tags,
raptor: {
use_raptor: data.use_raptor,
},
graphrag: {
use_graphrag: data.use_graphrag,
entity_types: ["organization", "person", "geo", "event", "category"],
method: data.graphrag_method,
},
},
pagerank: data.pagerank,
};
await knowledgeService.updateKnowledge(updateData);
setSuccess('知识库配置更新成功!');
// 延迟跳转到知识库列表页面
setTimeout(() => {
navigate('/knowledge');
}, 1500);
} catch (err: any) {
console.error('更新知识库配置失败:', err);
setError(err.response?.data?.message || err.message || '更新配置失败,请重试');
} finally {
setIsSubmitting(false);
}
};
// 跳过配置设置
const handleSkipConfig = () => {
setSuccess('知识库创建完成!');
setTimeout(() => {
navigate('/knowledge');
}, 1000);
};
const handleBack = () => {
if (activeStep === 0) {
navigate('/knowledge');
} else {
setActiveStep(0);
}
};
return (
<Box sx={{ p: 3, maxWidth: 900, mx: 'auto' }}>
{/* 页面标题 */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Button
startIcon={<ArrowBackIcon />}
onClick={handleBack}
sx={{ mr: 2 }}
>
{activeStep === 0 ? '返回' : '上一步'}
</Button>
<Typography variant="h4" component="h1" fontWeight={600}>
</Typography>
</Box>
{/* 步骤指示器 */}
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{/* 表单卡片 */}
<Card>
<CardContent sx={{ p: 4 }}>
{/* 错误和成功提示 */}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
{success}
</Alert>
)}
{/* 第一步:基础信息 */}
{activeStep === 0 && (
<Box component="form" onSubmit={basicForm.handleSubmit(handleBasicSubmit)} noValidate>
<Typography variant="h6" gutterBottom>
</Typography>
<Divider sx={{ mb: 3 }} />
<Grid container spacing={3}>
{/* 知识库名称 */}
<Grid size={12}>
<TextField
fullWidth
label="知识库名称"
placeholder="请输入知识库名称"
{...basicForm.register('name', {
required: '请输入知识库名称',
minLength: {
value: 2,
message: '知识库名称至少需要2个字符',
},
maxLength: {
value: 50,
message: '知识库名称不能超过50个字符',
},
})}
error={!!basicForm.formState.errors.name}
helperText={basicForm.formState.errors.name?.message}
/>
</Grid>
{/* 描述 */}
<Grid size={12}>
<TextField
fullWidth
multiline
rows={4}
label="描述"
placeholder="请输入知识库描述"
{...basicForm.register('description', {
maxLength: {
value: 500,
message: '描述不能超过500个字符',
},
})}
error={!!basicForm.formState.errors.description}
helperText={basicForm.formState.errors.description?.message}
/>
</Grid>
{/* 权限设置 */}
<Grid size={{xs:12, sm:6}}>
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select
label="权限设置"
{...basicForm.register('permission')}
>
<MenuItem value="me"></MenuItem>
<MenuItem value="team"></MenuItem>
<MenuItem value="public"></MenuItem>
</Select>
</FormControl>
</Grid>
{/* 提交按钮 */}
<Grid size={{xs:12}}>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', mt: 3 }}>
<Button
variant="outlined"
onClick={() => navigate('/knowledge')}
disabled={isSubmitting}
>
</Button>
<Button
type="submit"
variant="contained"
startIcon={isSubmitting ? <CircularProgress size={20} /> : <SaveIcon />}
disabled={isSubmitting}
>
{isSubmitting ? '创建中...' : '创建知识库'}
</Button>
</Box>
</Grid>
</Grid>
</Box>
)}
{/* 第二步:配置设置 */}
{activeStep === 1 && (
<Box component="form" onSubmit={configForm.handleSubmit(handleConfigSubmit)} noValidate>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CheckCircleIcon color="success" sx={{ mr: 1 }} />
<Typography variant="h6">
</Typography>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
</Typography>
<Divider sx={{ mb: 3 }} />
<Grid container spacing={3}>
{/* 解析器设置 */}
<Grid size={{xs:12}}>
<Typography variant="subtitle1" gutterBottom>
</Typography>
</Grid>
<Grid size={{xs:12,sm:6}}>
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select
label="解析器类型"
{...configForm.register('parser_id')}
>
<MenuItem value="naive"></MenuItem>
<MenuItem value="advanced"></MenuItem>
<MenuItem value="custom"></MenuItem>
</Select>
</FormControl>
</Grid>
<Grid size={{xs:12,sm:6}}>
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select
label="嵌入模型"
{...configForm.register('embd_id')}
>
<MenuItem value="text-embedding-v3@Tongyi-Qianwen"> v3</MenuItem>
<MenuItem value="text-embedding-v2@Tongyi-Qianwen"> v2</MenuItem>
<MenuItem value="openai-embedding">OpenAI Embedding</MenuItem>
</Select>
</FormControl>
</Grid>
{/* 分块设置 */}
<Grid size={{xs:12}}>
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2 }}>
</Typography>
</Grid>
<Grid size={{xs:12,sm:6}}>
<Typography gutterBottom>: {configForm.watch('chunk_token_num')}</Typography>
<Slider
{...configForm.register('chunk_token_num')}
value={configForm.watch('chunk_token_num')}
onChange={(_, value) => configForm.setValue('chunk_token_num', value as number)}
min={128}
max={2048}
step={128}
marks={[
{ value: 128, label: '128' },
{ value: 512, label: '512' },
{ value: 1024, label: '1024' },
{ value: 2048, label: '2048' },
]}
/>
</Grid>
<Grid size={{xs:12,sm:6}}>
<FormControl fullWidth>
<InputLabel></InputLabel>
<Select
label="布局识别"
{...configForm.register('layout_recognize')}
>
<MenuItem value="DeepDOC">DeepDOC</MenuItem>
<MenuItem value="OCR">OCR</MenuItem>
<MenuItem value="None"></MenuItem>
</Select>
</FormControl>
</Grid>
{/* 高级功能 */}
<Grid size={{xs:12}}>
<Typography variant="subtitle1" gutterBottom sx={{ mt: 2 }}>
</Typography>
</Grid>
<Grid size={{xs:12,sm:6}}>
<FormControlLabel
control={
<Switch
{...configForm.register('use_raptor')}
checked={configForm.watch('use_raptor')}
onChange={(e) => configForm.setValue('use_raptor', e.target.checked)}
/>
}
label="启用 Raptor"
/>
</Grid>
<Grid size={{xs:12,sm:6}}>
<FormControlLabel
control={
<Switch
{...configForm.register('use_graphrag')}
checked={configForm.watch('use_graphrag')}
onChange={(e) => configForm.setValue('use_graphrag', e.target.checked)}
/>
}
label="启用 GraphRAG"
/>
</Grid>
{configForm.watch('use_graphrag') && (
<Grid size={{xs:12,sm:6}}>
<FormControl fullWidth>
<InputLabel>GraphRAG </InputLabel>
<Select
label="GraphRAG 方法"
{...configForm.register('graphrag_method')}
>
<MenuItem value="light"></MenuItem>
<MenuItem value="standard"></MenuItem>
<MenuItem value="advanced"></MenuItem>
</Select>
</FormControl>
</Grid>
)}
{/* 操作按钮 */}
<Grid size={{xs:12}}>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', mt: 3 }}>
<Button
variant="outlined"
startIcon={<SkipNextIcon />}
onClick={handleSkipConfig}
disabled={isSubmitting}
>
</Button>
<Button
type="submit"
variant="contained"
startIcon={isSubmitting ? <CircularProgress size={20} /> : <SettingsIcon />}
disabled={isSubmitting}
>
{isSubmitting ? '配置中...' : '完成配置'}
</Button>
</Box>
</Grid>
</Grid>
</Box>
)}
</CardContent>
</Card>
</Box>
);
}
export default KnowledgeBaseCreate;

View File

@@ -0,0 +1,480 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
Box,
Typography,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Chip,
IconButton,
Button,
TextField,
InputAdornment,
LinearProgress,
Alert,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Menu,
MenuItem,
Tooltip,
Stack,
Card,
CardContent,
Grid,
Breadcrumbs,
Link,
} from '@mui/material';
import {
Search as SearchIcon,
Upload as UploadIcon,
Delete as DeleteIcon,
Refresh as RefreshIcon,
MoreVert as MoreVertIcon,
InsertDriveFile as FileIcon,
PictureAsPdf as PdfIcon,
Description as DocIcon,
Image as ImageIcon,
VideoFile as VideoIcon,
AudioFile as AudioIcon,
CloudUpload as CloudUploadIcon,
Settings as SettingsIcon,
} from '@mui/icons-material';
import knowledgeService from '@/services/knowledge_service';
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
import { RUNNING_STATUS_KEYS, type RunningStatus } from '@/constants/knowledge';
// 文件类型图标映射
const getFileIcon = (type: string) => {
const lowerType = type.toLowerCase();
if (lowerType.includes('pdf')) return <PdfIcon />;
if (lowerType.includes('doc') || lowerType.includes('txt') || lowerType.includes('md')) return <DocIcon />;
if (lowerType.includes('jpg') || lowerType.includes('png') || lowerType.includes('jpeg')) return <ImageIcon />;
if (lowerType.includes('mp4') || lowerType.includes('avi') || lowerType.includes('mov')) return <VideoIcon />;
if (lowerType.includes('mp3') || lowerType.includes('wav') || lowerType.includes('m4a')) return <AudioIcon />;
return <FileIcon />;
};
// 文件大小格式化
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
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, progress: number) => {
switch (status) {
case '1':
return <Chip label="已启用" color="success" size="small" />;
case '0':
return <Chip label="已禁用" color="default" size="small" />;
default:
return <Chip label="未知" color="warning" size="small" />;
}
};
// 运行状态映射
const getRunStatusChip = (run: RunningStatus, progress: number) => {
switch (run) {
case RUNNING_STATUS_KEYS.UNSTART:
return <Chip label="未开始" color="default" size="small" />;
case RUNNING_STATUS_KEYS.RUNNING:
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip label="解析中" color="primary" size="small" />
<Box sx={{ width: 60 }}>
<LinearProgress variant="determinate" value={progress} />
</Box>
<Typography variant="caption">{progress}%</Typography>
</Box>
);
case RUNNING_STATUS_KEYS.CANCEL:
return <Chip label="已取消" color="warning" size="small" />;
case RUNNING_STATUS_KEYS.DONE:
return <Chip label="已完成" color="success" size="small" />;
case RUNNING_STATUS_KEYS.FAIL:
return <Chip label="失败" color="error" size="small" />;
default:
return <Chip label="未知" color="default" size="small" />;
}
};
function KnowledgeBaseDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// 状态管理
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
const [files, setFiles] = useState<IKnowledgeFile[]>([]);
const [loading, setLoading] = useState(true);
const [filesLoading, setFilesLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchKeyword, setSearchKeyword] = useState('');
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
// 获取知识库详情
const fetchKnowledgeDetail = async () => {
if (!id) return;
try {
setLoading(true);
const response = await knowledgeService.getKnowledgeDetail({ kb_id: id });
if (response.data.code === 0) {
setKnowledgeBase(response.data.data);
} else {
setError(response.data.message || '获取知识库详情失败');
}
} catch (err: any) {
setError(err.response?.data?.message || err.message || '获取知识库详情失败');
} finally {
setLoading(false);
}
};
// 获取文件列表
const fetchFileList = async () => {
if (!id) return;
try {
setFilesLoading(true);
// const response = await knowledgeService.getDocumentList(
// { kb_id: id },
// { keywords: searchKeyword }
// );
// if (response.data.code === 0) {
// setFiles(response.data.data.docs || []);
// } else {
// setError(response.data.message || '获取文件列表失败');
// }
} catch (err: any) {
setError(err.response?.data?.message || err.message || '获取文件列表失败');
} finally {
setFilesLoading(false);
}
};
// 删除文件
const handleDeleteFiles = async () => {
if (selectedFiles.length === 0) return;
try {
await knowledgeService.removeDocument({ doc_ids: selectedFiles });
setSelectedFiles([]);
setDeleteDialogOpen(false);
fetchFileList(); // 刷新列表
} catch (err: any) {
setError(err.response?.data?.message || err.message || '删除文件失败');
}
};
// 重新解析文件
const handleReparse = async (docIds: string[]) => {
try {
await knowledgeService.runDocument({ doc_ids: docIds });
fetchFileList(); // 刷新列表
} catch (err: any) {
setError(err.response?.data?.message || err.message || '重新解析失败');
}
};
// 初始化数据
useEffect(() => {
fetchKnowledgeDetail();
// fetchFileList();
}, [id]);
// 搜索文件
useEffect(() => {
const timer = setTimeout(() => {
fetchFileList();
}, 500);
return () => clearTimeout(timer);
}, [searchKeyword]);
// 过滤文件
const filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchKeyword.toLowerCase())
);
if (loading) {
return (
<Box sx={{ p: 3 }}>
<LinearProgress />
<Typography sx={{ mt: 2 }}>...</Typography>
</Box>
);
}
if (error) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="error">{error}</Alert>
</Box>
);
}
if (!knowledgeBase) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="warning"></Alert>
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
{/* 面包屑导航 */}
<Breadcrumbs sx={{ mb: 2 }}>
<Link
component="button"
variant="body1"
onClick={() => navigate('/knowledge')}
sx={{ textDecoration: 'none' }}
>
</Link>
<Typography color="text.primary">{knowledgeBase.name}</Typography>
</Breadcrumbs>
{/* 知识库信息卡片 */}
<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}>
<Chip label={`${knowledgeBase.doc_num} 个文件`} variant="outlined" />
<Chip label={`${knowledgeBase.chunk_num} 个分块`} variant="outlined" />
<Chip label={`${knowledgeBase.token_num} 个令牌`} variant="outlined" />
</Stack>
</Grid>
<Grid size={{xs:12,md:4}}>
<Stack spacing={1} alignItems="flex-end">
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.create_date}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.update_date}
</Typography>
<Typography variant="body2" color="text.secondary">
: {knowledgeBase.language}
</Typography>
</Stack>
</Grid>
</Grid>
</CardContent>
</Card>
{/* 文件操作栏 */}
<Paper sx={{ p: 2, mb: 2 }}>
<Stack direction="row" spacing={2} alignItems="center" justifyContent="space-between">
<TextField
placeholder="搜索文件..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(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={() => setUploadDialogOpen(true)}
>
</Button>
{selectedFiles.length > 0 && (
<>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={() => handleReparse(selectedFiles)}
>
</Button>
<Button
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
onClick={() => setDeleteDialogOpen(true)}
>
({selectedFiles.length})
</Button>
</>
)}
<IconButton onClick={() => fetchFileList()}>
<RefreshIcon />
</IconButton>
</Stack>
</Stack>
</Paper>
{/* 文件列表 */}
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
{/* 全选复选框可以在这里添加 */}
</TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{filesLoading ? (
<TableRow>
<TableCell colSpan={9} align="center">
<LinearProgress />
<Typography sx={{ mt: 1 }}>...</Typography>
</TableCell>
</TableRow>
) : filteredFiles.length === 0 ? (
<TableRow>
<TableCell colSpan={9} align="center">
<Typography color="text.secondary">
{searchKeyword ? '没有找到匹配的文件' : '暂无文件'}
</Typography>
</TableCell>
</TableRow>
) : (
filteredFiles.map((file) => (
<TableRow key={file.id} hover>
<TableCell padding="checkbox">
{/* 文件选择复选框 */}
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getFileIcon(file.type)}
<Typography variant="body2">{file.name}</Typography>
</Box>
</TableCell>
<TableCell>
<Chip label={file.type.toUpperCase()} size="small" variant="outlined" />
</TableCell>
<TableCell>{formatFileSize(file.size)}</TableCell>
<TableCell>{file.chunk_num}</TableCell>
<TableCell>{getStatusChip(file.status, file.progress)}</TableCell>
<TableCell>{getRunStatusChip(file.run, file.progress)}</TableCell>
<TableCell>{file.create_date}</TableCell>
<TableCell>
<IconButton
size="small"
onClick={(e) => setAnchorEl(e.currentTarget)}
>
<MoreVertIcon />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
{/* 文件操作菜单 */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem onClick={() => setAnchorEl(null)}>
<RefreshIcon sx={{ mr: 1 }} />
</MenuItem>
<MenuItem onClick={() => setAnchorEl(null)}>
<SettingsIcon sx={{ mr: 1 }} />
</MenuItem>
<MenuItem onClick={() => setAnchorEl(null)} sx={{ color: 'error.main' }}>
<DeleteIcon sx={{ mr: 1 }} />
</MenuItem>
</Menu>
{/* 上传文件对话框 */}
<Dialog open={uploadDialogOpen} onClose={() => setUploadDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle></DialogTitle>
<DialogContent>
<Box
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 4,
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: 'primary.main',
backgroundColor: 'action.hover',
},
}}
>
<CloudUploadIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" gutterBottom>
</Typography>
<Typography variant="body2" color="text.secondary">
PDF, DOCX, TXT, MD, PNG, JPG, MP4, WAV
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setUploadDialogOpen(false)}></Button>
<Button variant="contained"></Button>
</DialogActions>
</Dialog>
{/* 删除确认对话框 */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle></DialogTitle>
<DialogContent>
<Typography>
{selectedFiles.length}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}></Button>
<Button color="error" onClick={handleDeleteFiles}></Button>
</DialogActions>
</Dialog>
</Box>
);
}
export default KnowledgeBaseDetail;

View File

@@ -19,13 +19,16 @@ import {
Refresh as RefreshIcon,
} from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { useKnowledgeList } from '@/hooks/knowledge_hooks';
import { useKnowledgeList, useKnowledgeOperations } from '@/hooks/knowledge_hooks';
import { useUserData } from '@/hooks/useUserData';
import KnowledgeGridView from '@/components/KnowledgeGridView';
import KnowledgeGridView from '@/components/knowledge/KnowledgeGridView';
import type { IKnowledge } from '@/interfaces/database/knowledge';
import { useDialog } from '@/hooks/useDialog';
const KnowledgeBaseList: React.FC = () => {
const navigate = useNavigate();
const {deleteKnowledge} = useKnowledgeOperations();
// 搜索和筛选状态
const [searchTerm, setSearchTerm] = useState('');
@@ -78,22 +81,32 @@ const KnowledgeBaseList: React.FC = () => {
navigate('/knowledge/create');
}, [navigate]);
// 处理编辑知识库
const handleEditKnowledge = useCallback((kb: IKnowledge) => {
navigate(`/knowledge/${kb.id}/edit`);
}, [navigate]);
const dialog = useDialog();
// 处理删除知识库
const handleDeleteKnowledge = useCallback(async (kb: IKnowledge) => {
// 需要确认删除
dialog.confirm({
title: '确认删除',
content: `是否确认删除知识库 ${kb.name}`,
onConfirm: async () => {
try {
await deleteKnowledge(kb.id);
// 删除成功后刷新列表
refresh();
} catch (err) {
console.error('Failed to delete knowledge:', err);
// 可以添加错误提示
}
},
});
}, [deleteKnowledge, refresh, dialog]);
// 处理查看知识库详情
const handleViewKnowledge = useCallback((kb: IKnowledge) => {
navigate(`/knowledge/${kb.id}`);
}, [navigate]);
// 处理删除知识库
const handleDeleteKnowledge = useCallback((kb: IKnowledge) => {
// TODO: 实现删除逻辑
console.log('删除知识库:', kb.id);
}, []);
// 根据团队筛选过滤知识库
const filteredKnowledgeBases = useMemo(() => {
if (!knowledgeBases) return [];
@@ -226,9 +239,8 @@ const KnowledgeBaseList: React.FC = () => {
<>
<KnowledgeGridView
knowledgeBases={currentPageData}
onEdit={handleEditKnowledge}
onDelete={handleDeleteKnowledge}
onView={handleViewKnowledge}
onDelete={handleDeleteKnowledge}
loading={loading}
searchTerm={searchTerm}
teamFilter={teamFilter}

View File

@@ -2,10 +2,12 @@ import { Routes, Route, Navigate } from 'react-router-dom';
import MainLayout from '../components/Layout/MainLayout';
import Login from '../pages/login/Login';
import Home from '../pages/Home';
import KnowledgeBaseList from '../pages/knowledge/KnowledgeBaseList';
import KnowledgeBaseList from '../pages/knowledge/list';
import PipelineConfig from '../pages/PipelineConfig';
import Dashboard from '../pages/Dashboard';
import ModelsResources from '../pages/ModelsResources';
import KnowledgeBaseCreate from '../pages/knowledge/create';
import KnowledgeBaseDetail from '../pages/knowledge/detail';
import MCP from '../pages/MCP';
const AppRoutes = () => {
@@ -16,6 +18,11 @@ const AppRoutes = () => {
{/* 使用MainLayout作为受保护路由的布局 */}
<Route path="/" element={<MainLayout />}>
{/* <Route index element={<Home />} /> */}
<Route path="knowledge">
<Route index element={<KnowledgeBaseList />} />
<Route path="create" element={<KnowledgeBaseCreate />} />
<Route path=":id" element={<KnowledgeBaseDetail />} />
</Route>
<Route index element={<KnowledgeBaseList />} />
<Route path="pipeline-config" element={<PipelineConfig />} />
<Route path="dashboard" element={<Dashboard />} />