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:
@@ -14,6 +14,7 @@
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^7.3.4",
|
||||
"@mui/material": "^7.3.4",
|
||||
"@mui/x-data-grid": "^8.14.0",
|
||||
"ahooks": "^3.9.5",
|
||||
"axios": "^1.12.2",
|
||||
"dayjs": "^1.11.18",
|
||||
|
||||
90
pnpm-lock.yaml
generated
90
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
||||
'@mui/material':
|
||||
specifier: ^7.3.4
|
||||
version: 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@mui/x-data-grid':
|
||||
specifier: ^8.14.0
|
||||
version: 8.14.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
ahooks:
|
||||
specifier: ^3.9.5
|
||||
version: 3.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -67,7 +70,7 @@ importers:
|
||||
version: 13.0.0
|
||||
zustand:
|
||||
specifier: ^5.0.8
|
||||
version: 5.0.8(@types/react@19.2.2)(react@18.3.1)
|
||||
version: 5.0.8(@types/react@19.2.2)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1))
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: ^9.36.0
|
||||
@@ -569,6 +572,35 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/x-data-grid@8.14.0':
|
||||
resolution: {integrity: sha512-bzUpD83Wx4mawkgquDQUUbLLnpF+JP7Pe7YQx1ixS6W/AlUwXAVagPTOijwchHvlx0Ky11dJvOQAfrnWu6an/Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.9.0
|
||||
'@emotion/styled': ^11.8.1
|
||||
'@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0
|
||||
'@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
|
||||
'@mui/x-internals@8.14.0':
|
||||
resolution: {integrity: sha512-esYyl61nuuFXiN631TWuPh2tqdoyTdBI/4UXgwH3rytF8jiWvy6prPBPRHEH1nvW3fgw9FoBI48FlOO+yEI8xg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@mui/x-virtualizer@0.2.3':
|
||||
resolution: {integrity: sha512-CZ+VxFmeJaTduAOlSyo5cVek0PV5Y8gm4coyaHEpCvms207J9AoMUKqWIcdwsVGlTH1Y71j35xT/MwHKutZiNw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1471,6 +1503,9 @@ packages:
|
||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
reselect@5.1.1:
|
||||
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
@@ -1590,6 +1625,11 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
uuid@13.0.0:
|
||||
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
|
||||
hasBin: true
|
||||
@@ -2116,6 +2156,45 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
|
||||
'@mui/x-data-grid@8.14.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@mui/material': 7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@mui/system': 7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1)
|
||||
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@18.3.1)
|
||||
'@mui/x-internals': 8.14.0(@types/react@19.2.2)(react@18.3.1)
|
||||
'@mui/x-virtualizer': 0.2.3(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
clsx: 2.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
use-sync-external-store: 1.6.0(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.2.2)(react@18.3.1)
|
||||
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@mui/x-internals@8.14.0(@types/react@19.2.2)(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
reselect: 5.1.1
|
||||
use-sync-external-store: 1.6.0(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@mui/x-virtualizer@0.2.3(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@mui/utils': 7.3.3(@types/react@19.2.2)(react@18.3.1)
|
||||
'@mui/x-internals': 8.14.0(@types/react@19.2.2)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -2984,6 +3063,8 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
reselect@5.1.1: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
@@ -3104,6 +3185,10 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
use-sync-external-store@1.6.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
uuid@13.0.0: {}
|
||||
|
||||
vite@7.1.9(@types/node@24.7.1):
|
||||
@@ -3132,7 +3217,8 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zustand@5.0.8(@types/react@19.2.2)(react@18.3.1):
|
||||
zustand@5.0.8(@types/react@19.2.2)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.2
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.6.0(react@18.3.1)
|
||||
|
||||
13
src/App.tsx
13
src/App.tsx
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
162
src/components/Provider/DialogComponent.tsx
Normal file
162
src/components/Provider/DialogComponent.tsx
Normal 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;
|
||||
117
src/components/Provider/DialogProvider.tsx
Normal file
117
src/components/Provider/DialogProvider.tsx
Normal 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;
|
||||
@@ -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;
|
||||
161
src/components/knowledge/KnowledgeGridView.tsx
Normal file
161
src/components/knowledge/KnowledgeGridView.tsx
Normal 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;
|
||||
193
src/examples/DialogExample.tsx
Normal file
193
src/examples/DialogExample.tsx
Normal 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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
12
src/hooks/useDialog.ts
Normal 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;
|
||||
};
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
503
src/pages/knowledge/create.tsx
Normal file
503
src/pages/knowledge/create.tsx
Normal 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;
|
||||
480
src/pages/knowledge/detail.tsx
Normal file
480
src/pages/knowledge/detail.tsx
Normal 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;
|
||||
@@ -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}
|
||||
@@ -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 />} />
|
||||
|
||||
Reference in New Issue
Block a user