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

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

View File

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

View File

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

View File

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

View File

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