From 5c937df5ed17f1cf8d6d11d2d4f130dc49e2e1fc Mon Sep 17 00:00:00 2001 From: "guangfei.zhao" Date: Mon, 13 Oct 2025 12:26:10 +0800 Subject: [PATCH] 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 --- package.json | 1 + pnpm-lock.yaml | 90 +++- src/App.tsx | 13 +- src/components/Layout/Header.tsx | 146 ++++- src/components/Provider/DialogComponent.tsx | 162 ++++++ src/components/Provider/DialogProvider.tsx | 117 ++++ .../KnowledgeCard.tsx} | 160 +----- .../knowledge/KnowledgeGridView.tsx | 161 ++++++ src/examples/DialogExample.tsx | 193 +++++++ src/hooks/knowledge_hooks.ts | 210 +++++++- src/hooks/login_hooks.ts | 2 + src/hooks/useDialog.ts | 12 + src/interfaces/common.ts | 34 ++ src/pages/Dashboard.tsx | 2 +- src/pages/knowledge/create.tsx | 503 ++++++++++++++++++ src/pages/knowledge/detail.tsx | 480 +++++++++++++++++ .../{KnowledgeBaseList.tsx => list.tsx} | 40 +- src/routes/index.tsx | 9 +- 18 files changed, 2151 insertions(+), 184 deletions(-) create mode 100644 src/components/Provider/DialogComponent.tsx create mode 100644 src/components/Provider/DialogProvider.tsx rename src/components/{KnowledgeGridView.tsx => knowledge/KnowledgeCard.tsx} (57%) create mode 100644 src/components/knowledge/KnowledgeGridView.tsx create mode 100644 src/examples/DialogExample.tsx create mode 100644 src/hooks/useDialog.ts create mode 100644 src/pages/knowledge/create.tsx create mode 100644 src/pages/knowledge/detail.tsx rename src/pages/knowledge/{KnowledgeBaseList.tsx => list.tsx} (89%) diff --git a/package.json b/package.json index a5c18c8..3ff1f00 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8df5acd..99ab5ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) diff --git a/src/App.tsx b/src/App.tsx index abf2297..1daf5cc 100644 --- a/src/App.tsx +++ b/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() { - - - - - + + + + + + + ); diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index a5de7a6..76b7629 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -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); + const open = Boolean(anchorEl); + + const handleAvatarClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleLogout = () => { + logout(); + handleClose(); + }; + return ( { > RAG Dashboard - + { placeholder="Search queries, KB names..." /> - + - - + + {/* 用户头像和菜单 */} + + {userInfo?.avatar ? ( + + ) : ( + + + + )} + + {/* 用户菜单 */} + + {/* 用户信息 */} + + + {userInfo?.nickname || '用户'} + + + {userInfo?.email} + + + + {/* 菜单项 */} + + + + + 个人资料 + + + + + + + + + 退出登录 + + + ); }; diff --git a/src/components/Provider/DialogComponent.tsx b/src/components/Provider/DialogComponent.tsx new file mode 100644 index 0000000..24d0cac --- /dev/null +++ b/src/components/Provider/DialogComponent.tsx @@ -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 = ({ 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 ; + case 'success': + return ; + case 'warning': + return ; + case 'error': + return ; + case 'confirm': + return ; + 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 ( + + {/* 标题栏 */} + + + {getDialogIcon()} + + {config.title || '提示'} + + + + + + + + {/* 内容区域 */} + + + {config.content} + + + + {/* 操作按钮 */} + + {config.showCancel !== false && ( + + )} + + + + ); +}; + +export default DialogComponent; \ No newline at end of file diff --git a/src/components/Provider/DialogProvider.tsx b/src/components/Provider/DialogProvider.tsx new file mode 100644 index 0000000..76fabf3 --- /dev/null +++ b/src/components/Provider/DialogProvider.tsx @@ -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(null); + +// 生成唯一ID的工具函数 +const generateId = () => `dialog_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + +export const DialogProvider: React.FC = ({ children }) => { + const [dialogs, setDialogs] = useState([]); + + // 打开对话框的通用方法 + const openDialog = useCallback((config: IDialogConfig): Promise => { + 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): Promise => { + return openDialog({ + ...config, + type: 'confirm', + showCancel: true, + confirmText: config.confirmText || '确定', + cancelText: config.cancelText || '取消', + }); + }, [openDialog]); + + // 信息对话框 + const info = useCallback((config: Omit): Promise => { + return openDialog({ + ...config, + type: 'info', + showCancel: false, + confirmText: config.confirmText || '确定', + }); + }, [openDialog]); + + // 成功对话框 + const success = useCallback((config: Omit): Promise => { + return openDialog({ + ...config, + type: 'success', + showCancel: false, + confirmText: config.confirmText || '确定', + }); + }, [openDialog]); + + // 警告对话框 + const warning = useCallback((config: Omit): Promise => { + return openDialog({ + ...config, + type: 'warning', + showCancel: false, + confirmText: config.confirmText || '确定', + }); + }, [openDialog]); + + // 错误对话框 + const error = useCallback((config: Omit): Promise => { + return openDialog({ + ...config, + type: 'error', + showCancel: false, + confirmText: config.confirmText || '确定', + }); + }, [openDialog]); + + const contextValue: IDialogContextValue = { + dialogs, + openDialog, + closeDialog, + confirm, + info, + success, + warning, + error, + }; + + return ( + + {children} + {/* 渲染所有对话框 */} + {dialogs.map(dialog => ( + closeDialog(dialog.id, result)} + /> + ))} + + ); +}; + +export default DialogProvider; \ No newline at end of file diff --git a/src/components/KnowledgeGridView.tsx b/src/components/knowledge/KnowledgeCard.tsx similarity index 57% rename from src/components/KnowledgeGridView.tsx rename to src/components/knowledge/KnowledgeCard.tsx index 3605438..95900cb 100644 --- a/src/components/KnowledgeGridView.tsx +++ b/src/components/knowledge/KnowledgeCard.tsx @@ -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, kb: IKnowledge) => void; + onViewKnowledge: (kb: IKnowledge) => void; } -const KnowledgeCard: React.FC = ({ knowledge, onMenuClick }) => { +const KnowledgeCard: React.FC = ({ knowledge, onMenuClick, onViewKnowledge }) => { const getStatusInfo = (permission: string) => { switch (permission) { case 'me': @@ -86,13 +72,13 @@ const KnowledgeCard: React.FC = ({ knowledge, onMenuClick }) }, }} > - + onViewKnowledge(knowledge)}> {/* 显示avatar */} {knowledge.avatar ? ( - ) : ( @@ -115,7 +101,10 @@ const KnowledgeCard: React.FC = ({ knowledge, onMenuClick }) /> onMenuClick(e, knowledge)} + onClick={(e) => { + e.stopPropagation(); // 阻止事件冒泡 + onMenuClick(e, knowledge); + }} > @@ -125,8 +114,8 @@ const KnowledgeCard: React.FC = ({ knowledge, onMenuClick }) = ({ knowledge, onMenuClick }) 最后更新: {formatUpdateTime(knowledge.update_time)} - + {/* 显示创建者 */} {knowledge.nickname && ( @@ -205,127 +194,4 @@ const KnowledgeCard: React.FC = ({ knowledge, onMenuClick }) ); }; -const KnowledgeGridView: React.FC = ({ - knowledgeBases, - maxItems, - showSeeAll = false, - onSeeAll, - onEdit, - onDelete, - onView, - loading = false, - searchTerm = '', - teamFilter = 'all', - onCreateKnowledge, -}) => { - const [anchorEl, setAnchorEl] = React.useState(null); - const [selectedKB, setSelectedKB] = React.useState(null); - - const handleMenuClick = (event: React.MouseEvent, 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 ( - - 加载中... - - ); - } - - if (knowledgeBases.length === 0) { - return ( - - - - {searchTerm || teamFilter !== 'all' ? '没有找到匹配的知识库' : '暂无知识库'} - - - {searchTerm || teamFilter !== 'all' - ? '尝试调整搜索条件或筛选器' - : '创建您的第一个知识库开始使用' - } - - {(!searchTerm && teamFilter === 'all' && onCreateKnowledge) && ( - - )} - - ); - } - - return ( - - - {displayedKBs.map((kb) => ( - - - - ))} - - - {showSeeAll && hasMore && ( - - - - )} - - - 查看详情 - 编辑 - 导出 - - 删除 - - - - ); -}; - -export default KnowledgeGridView; \ No newline at end of file +export default KnowledgeCard; \ No newline at end of file diff --git a/src/components/knowledge/KnowledgeGridView.tsx b/src/components/knowledge/KnowledgeGridView.tsx new file mode 100644 index 0000000..89aee1e --- /dev/null +++ b/src/components/knowledge/KnowledgeGridView.tsx @@ -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 = ({ + knowledgeBases, + maxItems, + showSeeAll = false, + onSeeAll, + onEdit, + onDelete, + onView, + loading = false, + searchTerm = '', + teamFilter = 'all', + onCreateKnowledge, +}) => { + const [anchorEl, setAnchorEl] = React.useState(null); + const [selectedKB, setSelectedKB] = React.useState(null); + + const handleMenuClick = (event: React.MouseEvent, 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 ( + + 加载中... + + ); + } + + if (knowledgeBases.length === 0) { + return ( + + + + {searchTerm || teamFilter !== 'all' ? '没有找到匹配的知识库' : '暂无知识库'} + + + {searchTerm || teamFilter !== 'all' + ? '尝试调整搜索条件或筛选器' + : '创建您的第一个知识库开始使用' + } + + {(!searchTerm && teamFilter === 'all' && onCreateKnowledge) && ( + + )} + + ); + } + + return ( + + + {displayedKBs.map((kb) => ( + + + + ))} + + + {showSeeAll && hasMore && ( + + + + )} + + + 查看详情 + + 删除 + + + + ); +}; + +export default KnowledgeGridView; \ No newline at end of file diff --git a/src/examples/DialogExample.tsx b/src/examples/DialogExample.tsx new file mode 100644 index 0000000..02e1687 --- /dev/null +++ b/src/examples/DialogExample.tsx @@ -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: ( + + + 这是一个自定义内容的对话框。 + + + 您可以在这里放置任何React组件。 + + + ), + type: 'confirm', + confirmText: '同意', + cancelText: '拒绝', + width: 600, + maskClosable: false, + onConfirm: async () => { + await new Promise(resolve => setTimeout(resolve, 500)); + console.log('用户同意了自定义操作'); + } + }); + + console.log('自定义对话框结果:', result); + }; + + return ( + + + Dialog 使用示例 + + + + 以下是各种类型对话框的使用示例: + + + + + + + + + + + + + + + + + + + 使用方法: + + +{`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({ ... });`} + + + + ); +}; + +export default DialogExample; \ No newline at end of file diff --git a/src/hooks/knowledge_hooks.ts b/src/hooks/knowledge_hooks.ts index bdeb93e..2a0096e 100644 --- a/src/hooks/knowledge_hooks.ts +++ b/src/hooks/knowledge_hooks.ts @@ -185,4 +185,212 @@ export const useKnowledgeDetail = (kbId: string) => { error, refresh: fetchKnowledgeDetail, }; -}; \ No newline at end of file +}; + +/** + * 知识库操作Hook + * 提供创建、更新、删除知识库的功能 + */ +export const useKnowledgeOperations = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + /** + * 创建知识库 + */ + const createKnowledge = useCallback(async (data: Partial) => { + 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; + 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(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, + }; +}; + diff --git a/src/hooks/login_hooks.ts b/src/hooks/login_hooks.ts index 146df28..2f1a15c 100644 --- a/src/hooks/login_hooks.ts +++ b/src/hooks/login_hooks.ts @@ -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'); }; diff --git a/src/hooks/useDialog.ts b/src/hooks/useDialog.ts new file mode 100644 index 0000000..33e7be7 --- /dev/null +++ b/src/hooks/useDialog.ts @@ -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; +}; diff --git a/src/interfaces/common.ts b/src/interfaces/common.ts index e0840bb..7ae0275 100644 --- a/src/interfaces/common.ts +++ b/src/interfaces/common.ts @@ -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; + 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; + closeDialog: (id: string, result?: boolean) => void; + confirm: (config: Omit) => Promise; + info: (config: Omit) => Promise; + success: (config: Omit) => Promise; + warning: (config: Omit) => Promise; + error: (config: Omit) => Promise; +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 43ea551..93eac28 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -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'; diff --git a/src/pages/knowledge/create.tsx b/src/pages/knowledge/create.tsx new file mode 100644 index 0000000..b3aeea7 --- /dev/null +++ b/src/pages/knowledge/create.tsx @@ -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(''); + const [success, setSuccess] = useState(''); + const [createdKbId, setCreatedKbId] = useState(''); + + // 基础信息表单 + const basicForm = useForm({ + defaultValues: { + name: '', + description: '', + permission: 'me', + avatar: undefined, + }, + }); + + // 配置设置表单 + const configForm = useForm({ + 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 ( + + {/* 页面标题 */} + + + + 创建知识库 + + + + {/* 步骤指示器 */} + + {steps.map((label) => ( + + {label} + + ))} + + + {/* 表单卡片 */} + + + {/* 错误和成功提示 */} + {error && ( + + {error} + + )} + + {success && ( + + {success} + + )} + + {/* 第一步:基础信息 */} + {activeStep === 0 && ( + + + 基础信息 + + + + + {/* 知识库名称 */} + + + + + {/* 描述 */} + + + + + {/* 权限设置 */} + + + 权限设置 + + + + + {/* 提交按钮 */} + + + + + + + + + )} + + {/* 第二步:配置设置 */} + {activeStep === 1 && ( + + + + + 知识库已创建成功,现在可以配置解析设置 + + + + 您可以现在配置这些设置,也可以稍后在知识库详情页面中修改 + + + + + {/* 解析器设置 */} + + + 解析器设置 + + + + + + 解析器类型 + + + + + + + 嵌入模型 + + + + + {/* 分块设置 */} + + + 分块设置 + + + + + 分块大小: {configForm.watch('chunk_token_num')} + 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' }, + ]} + /> + + + + + 布局识别 + + + + + {/* 高级功能 */} + + + 高级功能 + + + + + configForm.setValue('use_raptor', e.target.checked)} + /> + } + label="启用 Raptor" + /> + + + + configForm.setValue('use_graphrag', e.target.checked)} + /> + } + label="启用 GraphRAG" + /> + + + {configForm.watch('use_graphrag') && ( + + + GraphRAG 方法 + + + + )} + + {/* 操作按钮 */} + + + + + + + + + )} + + + + ); +} + +export default KnowledgeBaseCreate; diff --git a/src/pages/knowledge/detail.tsx b/src/pages/knowledge/detail.tsx new file mode 100644 index 0000000..e1e2f21 --- /dev/null +++ b/src/pages/knowledge/detail.tsx @@ -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 ; + if (lowerType.includes('doc') || lowerType.includes('txt') || lowerType.includes('md')) return ; + if (lowerType.includes('jpg') || lowerType.includes('png') || lowerType.includes('jpeg')) return ; + if (lowerType.includes('mp4') || lowerType.includes('avi') || lowerType.includes('mov')) return ; + if (lowerType.includes('mp3') || lowerType.includes('wav') || lowerType.includes('m4a')) return ; + return ; +}; + +// 文件大小格式化 +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 ; + case '0': + return ; + default: + return ; + } +}; + +// 运行状态映射 +const getRunStatusChip = (run: RunningStatus, progress: number) => { + switch (run) { + case RUNNING_STATUS_KEYS.UNSTART: + return ; + case RUNNING_STATUS_KEYS.RUNNING: + return ( + + + + + + {progress}% + + ); + case RUNNING_STATUS_KEYS.CANCEL: + return ; + case RUNNING_STATUS_KEYS.DONE: + return ; + case RUNNING_STATUS_KEYS.FAIL: + return ; + default: + return ; + } +}; + +function KnowledgeBaseDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + + // 状态管理 + const [knowledgeBase, setKnowledgeBase] = useState(null); + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(true); + const [filesLoading, setFilesLoading] = useState(false); + const [error, setError] = useState(null); + const [searchKeyword, setSearchKeyword] = useState(''); + const [selectedFiles, setSelectedFiles] = useState([]); + const [anchorEl, setAnchorEl] = useState(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 ( + + + 加载中... + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + if (!knowledgeBase) { + return ( + + 知识库不存在 + + ); + } + + return ( + + {/* 面包屑导航 */} + + navigate('/knowledge')} + sx={{ textDecoration: 'none' }} + > + 知识库 + + {knowledgeBase.name} + + + {/* 知识库信息卡片 */} + + + + + + {knowledgeBase.name} + + + {knowledgeBase.description || '暂无描述'} + + + + + + + + + + + 创建时间: {knowledgeBase.create_date} + + + 更新时间: {knowledgeBase.update_date} + + + 语言: {knowledgeBase.language} + + + + + + + + {/* 文件操作栏 */} + + + setSearchKeyword(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + sx={{ minWidth: 300 }} + size="small" + /> + + + + + {selectedFiles.length > 0 && ( + <> + + + + )} + + fetchFileList()}> + + + + + + + {/* 文件列表 */} + + + + + + {/* 全选复选框可以在这里添加 */} + + 文件名 + 类型 + 大小 + 分块数 + 状态 + 解析状态 + 上传时间 + 操作 + + + + {filesLoading ? ( + + + + 加载文件列表... + + + ) : filteredFiles.length === 0 ? ( + + + + {searchKeyword ? '没有找到匹配的文件' : '暂无文件'} + + + + ) : ( + filteredFiles.map((file) => ( + + + {/* 文件选择复选框 */} + + + + {getFileIcon(file.type)} + {file.name} + + + + + + {formatFileSize(file.size)} + {file.chunk_num} + {getStatusChip(file.status, file.progress)} + {getRunStatusChip(file.run, file.progress)} + {file.create_date} + + setAnchorEl(e.currentTarget)} + > + + + + + )) + )} + +
+
+ + {/* 文件操作菜单 */} + setAnchorEl(null)} + > + setAnchorEl(null)}> + + 重新解析 + + setAnchorEl(null)}> + + 解析设置 + + setAnchorEl(null)} sx={{ color: 'error.main' }}> + + 删除 + + + + {/* 上传文件对话框 */} + setUploadDialogOpen(false)} maxWidth="sm" fullWidth> + 上传文件 + + + + + 拖拽文件到此处或点击上传 + + + 支持 PDF, DOCX, TXT, MD, PNG, JPG, MP4, WAV 等格式 + + + + + + + + + + {/* 删除确认对话框 */} + setDeleteDialogOpen(false)}> + 确认删除 + + + 确定要删除选中的 {selectedFiles.length} 个文件吗?此操作不可撤销。 + + + + + + + +
+ ); +} + +export default KnowledgeBaseDetail; \ No newline at end of file diff --git a/src/pages/knowledge/KnowledgeBaseList.tsx b/src/pages/knowledge/list.tsx similarity index 89% rename from src/pages/knowledge/KnowledgeBaseList.tsx rename to src/pages/knowledge/list.tsx index 46e8a25..ede1770 100644 --- a/src/pages/knowledge/KnowledgeBaseList.tsx +++ b/src/pages/knowledge/list.tsx @@ -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 = () => { <> { @@ -16,6 +18,11 @@ const AppRoutes = () => { {/* 使用MainLayout作为受保护路由的布局 */} }> {/* } /> */} + + } /> + } /> + } /> + } /> } /> } />