feat(breadcrumbs): implement reusable breadcrumbs component and update usage
refactor(chunk): enhance chunk list with bulk operations and improved UI
This commit is contained in:
85
src/components/Breadcrumbs/BaseBreadcrumbs.tsx
Normal file
85
src/components/Breadcrumbs/BaseBreadcrumbs.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Breadcrumbs, Link, Typography, type SxProps, type Theme } from '@mui/material';
|
||||||
|
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
/** 显示文本 */
|
||||||
|
label: string;
|
||||||
|
/** 导航路径 */
|
||||||
|
path?: string;
|
||||||
|
/** 是否为最后一项(当前页面) */
|
||||||
|
isLast?: boolean;
|
||||||
|
/** 点击事件处理函数,优先级高于path */
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseBreadcrumbsProps {
|
||||||
|
/** 面包屑项目列表 */
|
||||||
|
items: BreadcrumbItem[];
|
||||||
|
/** 自定义样式 */
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
/** 分隔符 */
|
||||||
|
separator?: React.ReactNode;
|
||||||
|
/** 最大显示项目数 */
|
||||||
|
maxItems?: number;
|
||||||
|
/** 链接变体 */
|
||||||
|
linkVariant?: 'body1' | 'body2' | 'caption' | 'subtitle1' | 'subtitle2';
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseBreadcrumbs: React.FC<BaseBreadcrumbsProps> = ({
|
||||||
|
items,
|
||||||
|
sx,
|
||||||
|
separator,
|
||||||
|
maxItems,
|
||||||
|
linkVariant = 'body2'
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleItemClick = (item: BreadcrumbItem) => {
|
||||||
|
if (item.onClick) {
|
||||||
|
item.onClick();
|
||||||
|
} else if (item.path) {
|
||||||
|
navigate(item.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Breadcrumbs
|
||||||
|
sx={{ mb: 2, ...sx }}
|
||||||
|
separator={separator}
|
||||||
|
maxItems={maxItems}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
if (item.isLast) {
|
||||||
|
return (
|
||||||
|
<Typography key={index} color="text.primary">
|
||||||
|
{item.label}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={index}
|
||||||
|
component="button"
|
||||||
|
variant={linkVariant}
|
||||||
|
onClick={() => handleItemClick(item)}
|
||||||
|
sx={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
padding: 0,
|
||||||
|
font: 'inherit',
|
||||||
|
color: 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Breadcrumbs>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaseBreadcrumbs;
|
||||||
2
src/components/Breadcrumbs/index.ts
Normal file
2
src/components/Breadcrumbs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as BaseBreadcrumbs } from './BaseBreadcrumbs';
|
||||||
|
export type { BreadcrumbItem, BaseBreadcrumbsProps } from './BaseBreadcrumbs';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -15,16 +15,32 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Checkbox,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
DialogContentText,
|
||||||
|
Toolbar,
|
||||||
|
FormControlLabel,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
TextSnippet as TextIcon,
|
TextSnippet as TextIcon,
|
||||||
Visibility as VisibilityIcon,
|
Visibility as VisibilityIcon,
|
||||||
VisibilityOff as VisibilityOffIcon,
|
VisibilityOff as VisibilityOffIcon,
|
||||||
|
Delete as DeleteIcon,
|
||||||
|
ToggleOn as EnableIcon,
|
||||||
|
ToggleOff as DisableIcon,
|
||||||
|
SelectAll as SelectAllIcon,
|
||||||
|
Clear as ClearIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import type { IChunk } from '@/interfaces/database/knowledge';
|
import type { IChunk } from '@/interfaces/database/knowledge';
|
||||||
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
|
|
||||||
interface ChunkListResultProps {
|
interface ChunkListResultProps {
|
||||||
|
doc_id: string;
|
||||||
chunks: IChunk[];
|
chunks: IChunk[];
|
||||||
total: number;
|
total: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -32,11 +48,92 @@ interface ChunkListResultProps {
|
|||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
onPageChange: (page: number) => void;
|
onPageChange: (page: number) => void;
|
||||||
|
onRefresh?: () => void;
|
||||||
docName?: string;
|
docName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChunkListResult(props: ChunkListResultProps) {
|
function ChunkListResult(props: ChunkListResultProps) {
|
||||||
const { chunks, total, loading, error, page, pageSize, onPageChange, docName } = props;
|
const { doc_id, chunks, total, loading, error, page, pageSize, onPageChange, onRefresh, docName } = props;
|
||||||
|
|
||||||
|
// 选择状态
|
||||||
|
const [selectedChunks, setSelectedChunks] = useState<string[]>([]);
|
||||||
|
const [selectAll, setSelectAll] = useState(false);
|
||||||
|
|
||||||
|
// 操作状态
|
||||||
|
const [operationLoading, setOperationLoading] = useState(false);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// 处理单个选择
|
||||||
|
const handleSelectChunk = useCallback((chunkId: string, checked: boolean) => {
|
||||||
|
setSelectedChunks(prev => {
|
||||||
|
if (checked) {
|
||||||
|
return [...prev, chunkId];
|
||||||
|
} else {
|
||||||
|
return prev.filter(id => id !== chunkId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理全选
|
||||||
|
const handleSelectAll = useCallback((checked: boolean) => {
|
||||||
|
setSelectAll(checked);
|
||||||
|
if (checked) {
|
||||||
|
setSelectedChunks(chunks.map(chunk => chunk.chunk_id));
|
||||||
|
} else {
|
||||||
|
setSelectedChunks([]);
|
||||||
|
}
|
||||||
|
}, [chunks]);
|
||||||
|
|
||||||
|
// 清空选择
|
||||||
|
const handleClearSelection = useCallback(() => {
|
||||||
|
setSelectedChunks([]);
|
||||||
|
setSelectAll(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 启用/禁用chunks
|
||||||
|
const handleToggleChunks = useCallback(async (enable: boolean) => {
|
||||||
|
if (selectedChunks.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setOperationLoading(true);
|
||||||
|
await knowledgeService.switchChunk({
|
||||||
|
chunk_ids: selectedChunks,
|
||||||
|
available_int: enable ? 1 : 0,
|
||||||
|
doc_id: doc_id || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// delay 800 ms
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
// 清空选择并刷新
|
||||||
|
handleClearSelection();
|
||||||
|
onRefresh?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to toggle chunks:', err);
|
||||||
|
} finally {
|
||||||
|
setOperationLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedChunks, handleClearSelection, onRefresh]);
|
||||||
|
|
||||||
|
// 删除chunks
|
||||||
|
const handleDeleteChunks = useCallback(async () => {
|
||||||
|
if (selectedChunks.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setOperationLoading(true);
|
||||||
|
await knowledgeService.removeChunk({
|
||||||
|
chunk_ids: selectedChunks
|
||||||
|
});
|
||||||
|
|
||||||
|
// 关闭对话框,清空选择并刷新
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
handleClearSelection();
|
||||||
|
onRefresh?.();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete chunks:', err);
|
||||||
|
} finally {
|
||||||
|
setOperationLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedChunks, handleClearSelection, onRefresh]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -73,50 +170,85 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalPages = Math.ceil(total / pageSize);
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
|
const enabledCount = chunks.filter(chunk => chunk.available_int === 1).length;
|
||||||
|
const selectedEnabledCount = selectedChunks.filter(id =>
|
||||||
|
chunks.find(chunk => chunk.chunk_id === id)?.available_int === 1
|
||||||
|
).length;
|
||||||
|
const selectedDisabledCount = selectedChunks.length - selectedEnabledCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* Chunk结果概览 */}
|
{/* 批量操作工具栏 */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ mb: 3 }}>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Toolbar sx={{ px: 2, py: 1 }}>
|
||||||
文档Chunk详情
|
<FormControlLabel
|
||||||
</Typography>
|
control={
|
||||||
{docName && (
|
<Checkbox
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
checked={selectAll}
|
||||||
文档名称: {docName}
|
indeterminate={selectedChunks.length > 0 && selectedChunks.length < chunks.length}
|
||||||
</Typography>
|
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||||
)}
|
/>
|
||||||
<Grid container spacing={2}>
|
}
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
label={`全选 (已选择 ${selectedChunks.length} 个)`}
|
||||||
<Card>
|
/>
|
||||||
<CardContent>
|
|
||||||
<Typography variant="h4" color="primary">
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
{total}
|
|
||||||
</Typography>
|
{selectedChunks.length > 0 && (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Stack direction="row" spacing={1}>
|
||||||
总Chunk数量
|
{selectedDisabledCount > 0 && (
|
||||||
</Typography>
|
<Button
|
||||||
</CardContent>
|
startIcon={<EnableIcon />}
|
||||||
</Card>
|
onClick={() => handleToggleChunks(true)}
|
||||||
</Grid>
|
disabled={operationLoading}
|
||||||
<Grid size={{ xs: 12, sm: 6 }}>
|
color="success"
|
||||||
<Card>
|
variant="outlined"
|
||||||
<CardContent>
|
size="small"
|
||||||
<Typography variant="h4" color="secondary">
|
>
|
||||||
{chunks.filter(chunk => chunk.available_int === 1).length}
|
启用 ({selectedDisabledCount})
|
||||||
</Typography>
|
</Button>
|
||||||
<Typography variant="body2" color="text.secondary">
|
)}
|
||||||
已启用Chunk
|
|
||||||
</Typography>
|
{selectedEnabledCount > 0 && (
|
||||||
</CardContent>
|
<Button
|
||||||
</Card>
|
startIcon={<DisableIcon />}
|
||||||
</Grid>
|
onClick={() => handleToggleChunks(false)}
|
||||||
</Grid>
|
disabled={operationLoading}
|
||||||
|
color="warning"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
禁用 ({selectedEnabledCount})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
|
disabled={operationLoading}
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
删除 ({selectedChunks.length})
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
startIcon={<ClearIcon />}
|
||||||
|
onClick={handleClearSelection}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
清空选择
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Toolbar>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Chunk列表 */}
|
{/* Chunk列表 */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
Chunk列表 (第 {page} 页,共 {totalPages} 页)
|
Chunk列表 (第 {page} 页,共 {totalPages} 页)
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -125,7 +257,7 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={2}>
|
||||||
{chunks.map((chunk, index) => (
|
{chunks.map((chunk, index) => (
|
||||||
<Grid size={12} key={chunk.chunk_id}>
|
<Grid size={12} key={chunk.chunk_id}>
|
||||||
<Card
|
<Card
|
||||||
@@ -133,38 +265,50 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
sx={{
|
sx={{
|
||||||
transition: 'all 0.2s ease-in-out',
|
transition: 'all 0.2s ease-in-out',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
boxShadow: 3,
|
boxShadow: 2,
|
||||||
transform: 'translateY(-2px)',
|
|
||||||
},
|
},
|
||||||
border: chunk.available_int === 1 ? '2px solid' : '1px solid',
|
border: selectedChunks.includes(chunk.chunk_id) ? '2px solid' : '1px solid',
|
||||||
borderColor: chunk.available_int === 1 ? 'success.main' : 'grey.300',
|
borderColor: selectedChunks.includes(chunk.chunk_id)
|
||||||
|
? 'primary.main'
|
||||||
|
: chunk.available_int === 1
|
||||||
|
? 'success.light'
|
||||||
|
: 'grey.300',
|
||||||
|
backgroundColor: selectedChunks.includes(chunk.chunk_id) ? 'action.selected' : 'background.paper',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent sx={{ p: 3 }}>
|
<CardContent sx={{ p: 2 }}>
|
||||||
{/* 头部信息 */}
|
{/* 头部信息 */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedChunks.includes(chunk.chunk_id)}
|
||||||
|
onChange={(e) => handleSelectChunk(chunk.chunk_id, e.target.checked)}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
<Avatar
|
<Avatar
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: chunk.available_int === 1 ? 'success.main' : 'grey.400',
|
bgcolor: chunk.available_int === 1 ? 'success.main' : 'grey.400',
|
||||||
mr: 2,
|
mr: 2,
|
||||||
width: 40,
|
width: 32,
|
||||||
height: 40,
|
height: 32,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextIcon />
|
<TextIcon fontSize="small" />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Typography variant="h6" fontWeight="bold" color="text.primary">
|
<Typography variant="subtitle1" fontWeight="medium">
|
||||||
Chunk #{((page - 1) * pageSize) + index + 1}
|
Chunk #{((page - 1) * pageSize) + index + 1}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
ID: {chunk.chunk_id}
|
ID: {chunk.chunk_id}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
{chunk.image_id && (
|
{chunk.image_id && (
|
||||||
<Tooltip title="包含图片">
|
<Tooltip title="包含图片">
|
||||||
<Avatar sx={{ bgcolor: 'info.main', width: 32, height: 32 }}>
|
<Avatar sx={{ bgcolor: 'info.main', width: 24, height: 24 }}>
|
||||||
<ImageIcon fontSize="small" />
|
<ImageIcon fontSize="small" />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -179,39 +323,27 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ mb: 2 }} />
|
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
内容预览
|
内容预览
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paper
|
<Paper
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={{
|
||||||
p: 2,
|
p: 1.5,
|
||||||
maxHeight: '200px',
|
maxHeight: '150px',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
backgroundColor: 'grey.50',
|
backgroundColor: 'grey.50',
|
||||||
borderRadius: 2,
|
borderRadius: 1,
|
||||||
'&::-webkit-scrollbar': {
|
fontSize: '0.875rem',
|
||||||
width: '6px',
|
lineHeight: 1.5,
|
||||||
},
|
|
||||||
'&::-webkit-scrollbar-track': {
|
|
||||||
backgroundColor: 'grey.100',
|
|
||||||
borderRadius: '3px',
|
|
||||||
},
|
|
||||||
'&::-webkit-scrollbar-thumb': {
|
|
||||||
backgroundColor: 'grey.400',
|
|
||||||
borderRadius: '3px',
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
lineHeight: 1.6,
|
|
||||||
color: 'text.primary',
|
color: 'text.primary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -222,17 +354,17 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
|
|
||||||
{/* 图片显示区域 */}
|
{/* 图片显示区域 */}
|
||||||
{chunk.image_id && (
|
{chunk.image_id && (
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 1 }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
相关图片
|
相关图片
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paper
|
<Box
|
||||||
variant="outlined"
|
|
||||||
sx={{
|
sx={{
|
||||||
p: 2,
|
|
||||||
borderRadius: 2,
|
|
||||||
backgroundColor: 'background.paper',
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
p: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'grey.300',
|
||||||
|
borderRadius: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -241,33 +373,32 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
alt="Chunk相关图片"
|
alt="Chunk相关图片"
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
maxHeight: '300px',
|
maxHeight: '200px',
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
boxShadow: 1,
|
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
target.style.display = 'none';
|
target.style.display = 'none';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 关键词区域 */}
|
{/* 关键词区域 */}
|
||||||
{((chunk.important_kwd ?? []).length > 0 || (chunk.question_kwd ?? []).length > 0 || (chunk.tag_kwd ?? []).length > 0) && (
|
{((chunk.important_kwd ?? []).length > 0 || (chunk.question_kwd ?? []).length > 0 || (chunk.tag_kwd ?? []).length > 0) && (
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box>
|
||||||
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 2 }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||||
关键词信息
|
关键词信息
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{chunk.important_kwd && chunk.important_kwd.length > 0 && (
|
<Stack spacing={1}>
|
||||||
<Box sx={{ mb: 2 }}>
|
{chunk.important_kwd && chunk.important_kwd.length > 0 && (
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
<Box>
|
||||||
重要关键词
|
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}>
|
||||||
</Typography>
|
重要:
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
</Typography>
|
||||||
{chunk.important_kwd.map((keyword, kwdIndex) => (
|
{chunk.important_kwd.map((keyword, kwdIndex) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={kwdIndex}
|
key={kwdIndex}
|
||||||
@@ -275,19 +406,17 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ fontWeight: 'medium' }}
|
sx={{ mr: 0.5, mb: 0.5, fontSize: '0.75rem' }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{chunk.question_kwd && chunk.question_kwd.length > 0 && (
|
{chunk.question_kwd && chunk.question_kwd.length > 0 && (
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}>
|
||||||
问题关键词
|
问题:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
|
||||||
{chunk.question_kwd.map((keyword, kwdIndex) => (
|
{chunk.question_kwd.map((keyword, kwdIndex) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={kwdIndex}
|
key={kwdIndex}
|
||||||
@@ -295,19 +424,17 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
sx={{ fontWeight: 'medium' }}
|
sx={{ mr: 0.5, mb: 0.5, fontSize: '0.75rem' }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{chunk.tag_kwd && chunk.tag_kwd.length > 0 && (
|
{chunk.tag_kwd && chunk.tag_kwd.length > 0 && (
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}>
|
||||||
标签关键词
|
标签:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
|
||||||
{chunk.tag_kwd.map((keyword, kwdIndex) => (
|
{chunk.tag_kwd.map((keyword, kwdIndex) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={kwdIndex}
|
key={kwdIndex}
|
||||||
@@ -315,25 +442,12 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="info"
|
color="info"
|
||||||
sx={{ fontWeight: 'medium' }}
|
sx={{ mr: 0.5, mb: 0.5, fontSize: '0.75rem' }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
)}
|
</Stack>
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 位置信息 */}
|
|
||||||
{chunk.positions && chunk.positions.length > 0 && (
|
|
||||||
<Box sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'grey.200' }}>
|
|
||||||
<Chip
|
|
||||||
label={`位置信息: ${chunk.positions.length} 个位置点`}
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
color="default"
|
|
||||||
sx={{ fontWeight: 'medium' }}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -356,6 +470,31 @@ function ChunkListResult(props: ChunkListResultProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* 删除确认对话框 */}
|
||||||
|
<Dialog
|
||||||
|
open={deleteDialogOpen}
|
||||||
|
onClose={() => setDeleteDialogOpen(false)}
|
||||||
|
>
|
||||||
|
<DialogTitle>确认删除</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
确定要删除选中的 {selectedChunks.length} 个chunk吗?此操作不可撤销。
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDeleteDialogOpen(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDeleteChunks}
|
||||||
|
color="error"
|
||||||
|
disabled={operationLoading}
|
||||||
|
>
|
||||||
|
{operationLoading ? '删除中...' : '确认删除'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Breadcrumbs,
|
|
||||||
Link,
|
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardMedia,
|
CardMedia,
|
||||||
@@ -20,6 +18,7 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import knowledgeService from '@/services/knowledge_service';
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||||
|
import KnowledgeBreadcrumbs from '@/pages/knowledge/components/KnowledgeBreadcrumbs';
|
||||||
|
|
||||||
interface DocumentPreviewProps {}
|
interface DocumentPreviewProps {}
|
||||||
|
|
||||||
@@ -34,6 +33,9 @@ function DocumentPreview(props: DocumentPreviewProps) {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [fileLoading, setFileLoading] = useState(false);
|
const [fileLoading, setFileLoading] = useState(false);
|
||||||
|
|
||||||
|
// 用于取消请求的AbortController
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
// 获取知识库和文档信息
|
// 获取知识库和文档信息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,6 +62,9 @@ function DocumentPreview(props: DocumentPreviewProps) {
|
|||||||
if (docResponse.data.data?.length > 0) {
|
if (docResponse.data.data?.length > 0) {
|
||||||
setDocumentObj(docResponse.data.data[0]);
|
setDocumentObj(docResponse.data.data[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动开始预览文件
|
||||||
|
loadDocumentFile();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('获取数据失败:', err);
|
console.error('获取数据失败:', err);
|
||||||
setError('获取数据失败,请稍后重试');
|
setError('获取数据失败,请稍后重试');
|
||||||
@@ -79,7 +84,19 @@ function DocumentPreview(props: DocumentPreviewProps) {
|
|||||||
setFileLoading(true);
|
setFileLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const fileResponse = await knowledgeService.getDocumentFile({ doc_id });
|
// 取消之前的请求
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的AbortController
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
|
const fileResponse = await knowledgeService.getDocumentFile({
|
||||||
|
doc_id
|
||||||
|
}, {
|
||||||
|
signal: abortControllerRef.current.signal
|
||||||
|
});
|
||||||
|
|
||||||
if (fileResponse.data instanceof Blob) {
|
if (fileResponse.data instanceof Blob) {
|
||||||
setDocumentFile(fileResponse.data);
|
setDocumentFile(fileResponse.data);
|
||||||
@@ -88,20 +105,27 @@ function DocumentPreview(props: DocumentPreviewProps) {
|
|||||||
} else {
|
} else {
|
||||||
setError('文件格式不支持预览');
|
setError('文件格式不支持预览');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error('获取文档文件失败:', err);
|
console.log('err', err);
|
||||||
setError('获取文档文件失败,请稍后重试');
|
if (err.name !== 'AbortError' && err.name !== 'CanceledError') {
|
||||||
|
console.error('获取文档文件失败:', err);
|
||||||
|
setError('获取文档文件失败,请稍后重试');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setFileLoading(false);
|
setFileLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清理文件URL
|
// 清理fileUrl
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (fileUrl) {
|
if (fileUrl) {
|
||||||
URL.revokeObjectURL(fileUrl);
|
URL.revokeObjectURL(fileUrl);
|
||||||
}
|
}
|
||||||
|
// 组件卸载时取消正在进行的请求
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [fileUrl]);
|
}, [fileUrl]);
|
||||||
|
|
||||||
@@ -119,6 +143,17 @@ function DocumentPreview(props: DocumentPreviewProps) {
|
|||||||
|
|
||||||
// 返回上一页
|
// 返回上一页
|
||||||
const handleGoBack = () => {
|
const handleGoBack = () => {
|
||||||
|
// 取消正在进行的请求
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理文件URL
|
||||||
|
if (fileUrl) {
|
||||||
|
URL.revokeObjectURL(fileUrl);
|
||||||
|
setFileUrl('');
|
||||||
|
}
|
||||||
|
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,33 +239,27 @@ function DocumentPreview(props: DocumentPreviewProps) {
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
<Breadcrumbs sx={{ mb: 2 }}>
|
<KnowledgeBreadcrumbs
|
||||||
<Link
|
kbItems={[
|
||||||
component="button"
|
{
|
||||||
variant="body2"
|
label: '知识库',
|
||||||
onClick={() => navigate('/knowledge')}
|
path: '/knowledge'
|
||||||
sx={{ textDecoration: 'none' }}
|
},
|
||||||
>
|
{
|
||||||
知识库
|
label: kb?.name || '知识库详情',
|
||||||
</Link>
|
path: `/knowledge/${kb_id}`
|
||||||
<Link
|
}
|
||||||
component="button"
|
]}
|
||||||
variant="body2"
|
extraItems={[
|
||||||
onClick={() => navigate(`/knowledge/${kb_id}`)}
|
{
|
||||||
sx={{ textDecoration: 'none' }}
|
label: documentObj?.name || '文档详情',
|
||||||
>
|
path: `/chunk/parsed-result?kb_id=${kb_id}&doc_id=${doc_id}`
|
||||||
{kb?.name || '知识库详情'}
|
},
|
||||||
</Link>
|
{
|
||||||
<Link
|
label: '文件预览'
|
||||||
component="button"
|
}
|
||||||
variant="body2"
|
]}
|
||||||
onClick={() => navigate(`/knowledge/${kb_id}/document/${doc_id}/chunks`)}
|
/>
|
||||||
sx={{ textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
文档分块
|
|
||||||
</Link>
|
|
||||||
<Typography color="text.primary">文件预览</Typography>
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
{/* 页面标题和操作按钮 */}
|
{/* 页面标题和操作按钮 */}
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
@@ -254,17 +283,6 @@ function DocumentPreview(props: DocumentPreviewProps) {
|
|||||||
返回
|
返回
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{!fileUrl && (
|
|
||||||
<Button
|
|
||||||
startIcon={<VisibilityIcon />}
|
|
||||||
onClick={loadDocumentFile}
|
|
||||||
variant="contained"
|
|
||||||
disabled={fileLoading}
|
|
||||||
>
|
|
||||||
{fileLoading ? '加载中...' : '预览文件'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fileUrl && (
|
{fileUrl && (
|
||||||
<Button
|
<Button
|
||||||
startIcon={<DownloadIcon />}
|
startIcon={<DownloadIcon />}
|
||||||
|
|||||||
@@ -3,19 +3,20 @@ import { useSearchParams, useNavigate } from "react-router-dom";
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Breadcrumbs,
|
|
||||||
Link,
|
|
||||||
TextField,
|
TextField,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
Paper,
|
Paper,
|
||||||
Alert,
|
Alert,
|
||||||
Button
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Search as SearchIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
|
import { Search as SearchIcon, Visibility as VisibilityIcon } from '@mui/icons-material';
|
||||||
import { useChunkList } from '@/hooks/chunk-hooks';
|
import { useChunkList } from '@/hooks/chunk-hooks';
|
||||||
import ChunkListResult from './components/ChunkListResult';
|
import ChunkListResult from './components/ChunkListResult';
|
||||||
import knowledgeService from '@/services/knowledge_service';
|
import knowledgeService from '@/services/knowledge_service';
|
||||||
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
|
||||||
|
import KnowledgeBreadcrumbs from '@/pages/knowledge/components/KnowledgeBreadcrumbs';
|
||||||
|
|
||||||
function ChunkParsedResult() {
|
function ChunkParsedResult() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -53,6 +54,7 @@ function ChunkParsedResult() {
|
|||||||
// 获取知识库信息
|
// 获取知识库信息
|
||||||
const kbResponse = await knowledgeService.getKnowledgeDetail({ kb_id });
|
const kbResponse = await knowledgeService.getKnowledgeDetail({ kb_id });
|
||||||
if (kbResponse.data.code === 0) {
|
if (kbResponse.data.code === 0) {
|
||||||
|
console.log('KnowledgeBase:', kbResponse.data);
|
||||||
setKnowledgeBase(kbResponse.data.data);
|
setKnowledgeBase(kbResponse.data.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,34 +102,23 @@ function ChunkParsedResult() {
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
<Box sx={{ mb: 3 }}>
|
<KnowledgeBreadcrumbs
|
||||||
<Breadcrumbs>
|
kbItems={[
|
||||||
<Link
|
{
|
||||||
color="inherit"
|
label: '知识库',
|
||||||
href="#"
|
path: '/knowledge'
|
||||||
onClick={(e) => {
|
},
|
||||||
e.preventDefault();
|
{
|
||||||
navigate('/knowledge');
|
label: knowledgeBase?.name || '知识库详情',
|
||||||
}}
|
path: `/knowledge/${kb_id}`
|
||||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
|
}
|
||||||
>
|
]}
|
||||||
知识库列表
|
extraItems={[
|
||||||
</Link>
|
{
|
||||||
<Link
|
label: document?.name || '文档详情'
|
||||||
color="inherit"
|
},
|
||||||
href="#"
|
]}
|
||||||
onClick={(e) => {
|
/>
|
||||||
e.preventDefault();
|
|
||||||
navigate(`/knowledge/${kb_id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{knowledgeBase?.name || '知识库详情'}
|
|
||||||
</Link>
|
|
||||||
<Typography color="text.primary">
|
|
||||||
{document?.name || '文档Chunk详情'}
|
|
||||||
</Typography>
|
|
||||||
</Breadcrumbs>
|
|
||||||
</Box>
|
|
||||||
{/* 页面标题和文档信息 */}
|
{/* 页面标题和文档信息 */}
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
<Paper sx={{ p: 3, mb: 3 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||||
@@ -139,6 +130,16 @@ function ChunkParsedResult() {
|
|||||||
查看文档 "{document?.name}" 的所有chunk数据
|
查看文档 "{document?.name}" 的所有chunk数据
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Card>
|
||||||
|
<CardContent sx={{ textAlign: 'center' }}>
|
||||||
|
<Typography variant="h4" color="primary">
|
||||||
|
{total}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
总Chunk数量
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<VisibilityIcon />}
|
startIcon={<VisibilityIcon />}
|
||||||
@@ -169,6 +170,7 @@ function ChunkParsedResult() {
|
|||||||
|
|
||||||
{/* Chunk列表结果 */}
|
{/* Chunk列表结果 */}
|
||||||
<ChunkListResult
|
<ChunkListResult
|
||||||
|
doc_id={doc_id}
|
||||||
chunks={chunks}
|
chunks={chunks}
|
||||||
total={total}
|
total={total}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -176,6 +178,7 @@ function ChunkParsedResult() {
|
|||||||
page={currentPage}
|
page={currentPage}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
|
onRefresh={refresh}
|
||||||
docName={document?.name}
|
docName={document?.name}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,88 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import type { SxProps, Theme } from '@mui/material';
|
||||||
import { Breadcrumbs, Link, Typography } from '@mui/material';
|
import { BaseBreadcrumbs, type BreadcrumbItem } from '@/components/Breadcrumbs';
|
||||||
import type { IKnowledge } from '@/interfaces/database/knowledge';
|
|
||||||
|
|
||||||
interface KnowledgeBreadcrumbsProps {
|
interface KnowledgeBreadcrumbsProps {
|
||||||
knowledge?: IKnowledge | null;
|
sx?: SxProps<Theme>;
|
||||||
sx?: object;
|
/** 知识库相关的面包屑项目,可以完全自定义 */
|
||||||
|
kbItems?: BreadcrumbItem[];
|
||||||
|
/** 额外的面包屑项目,会添加到kbItems之后 */
|
||||||
|
extraItems?: BreadcrumbItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const KnowledgeBreadcrumbs: React.FC<KnowledgeBreadcrumbsProps> = ({ knowledge, sx }) => {
|
const KnowledgeBreadcrumbs: React.FC<KnowledgeBreadcrumbsProps> = ({
|
||||||
const location = useLocation();
|
sx,
|
||||||
const navigate = useNavigate();
|
kbItems = [],
|
||||||
const { id } = useParams<{ id: string }>();
|
extraItems = []
|
||||||
|
}) => {
|
||||||
// 解析当前路径
|
// 合并所有面包屑项目
|
||||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
const allItems = [...kbItems, ...extraItems];
|
||||||
|
|
||||||
// 生成面包屑项
|
// 确保最后一个项目被标记为最后一项
|
||||||
const breadcrumbItems = [];
|
const breadcrumbItems = allItems.map((item, index) => ({
|
||||||
|
...item,
|
||||||
// 第一层:知识库列表
|
isLast: index === allItems.length - 1
|
||||||
breadcrumbItems.push({
|
}));
|
||||||
label: '知识库',
|
|
||||||
path: '/knowledge',
|
|
||||||
isLast: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// 第二层:知识库详情(如果有id)
|
|
||||||
if (id && knowledge) {
|
|
||||||
const isDetailPage = pathSegments.length === 2; // /knowledge/:id
|
|
||||||
breadcrumbItems.push({
|
|
||||||
label: knowledge.name,
|
|
||||||
path: `/knowledge/${id}`,
|
|
||||||
isLast: isDetailPage
|
|
||||||
});
|
|
||||||
|
|
||||||
// 第三层:设置或测试页面
|
|
||||||
if (pathSegments.length === 3) {
|
|
||||||
const lastSegment = pathSegments[2];
|
|
||||||
let label = '';
|
|
||||||
|
|
||||||
switch (lastSegment) {
|
|
||||||
case 'setting':
|
|
||||||
label = '设置';
|
|
||||||
break;
|
|
||||||
case 'testing':
|
|
||||||
label = '测试';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
label = lastSegment;
|
|
||||||
}
|
|
||||||
|
|
||||||
breadcrumbItems.push({
|
|
||||||
label,
|
|
||||||
path: location.pathname,
|
|
||||||
isLast: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breadcrumbs sx={{ mb: 2, ...sx }}>
|
<BaseBreadcrumbs
|
||||||
{breadcrumbItems.map((item, index) => {
|
items={breadcrumbItems}
|
||||||
if (item.isLast) {
|
sx={sx}
|
||||||
return (
|
linkVariant="body1"
|
||||||
<Typography key={index} color="text.primary">
|
/>
|
||||||
{item.label}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={index}
|
|
||||||
component="button"
|
|
||||||
variant="body1"
|
|
||||||
onClick={() => navigate(item.path)}
|
|
||||||
sx={{ textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Breadcrumbs>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ function KnowledgeBaseCreate() {
|
|||||||
<Divider sx={{ my: 3 }} />
|
<Divider sx={{ my: 3 }} />
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
{activeStep === 0 && (
|
{activeStep === 0 && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -275,7 +275,18 @@ function KnowledgeBaseDetail() {
|
|||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
<KnowledgeBreadcrumbs knowledge={knowledgeBase} />
|
<KnowledgeBreadcrumbs
|
||||||
|
kbItems={[
|
||||||
|
{
|
||||||
|
label: '知识库',
|
||||||
|
path: '/knowledge'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: knowledgeBase?.name || '知识库详情',
|
||||||
|
path: `/knowledge/${id}`
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 知识库信息卡片 */}
|
{/* 知识库信息卡片 */}
|
||||||
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
|
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
|
||||||
|
|||||||
@@ -157,7 +157,21 @@ function KnowledgeBaseSetting() {
|
|||||||
return (
|
return (
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
<KnowledgeBreadcrumbs knowledge={knowledge} />
|
<KnowledgeBreadcrumbs
|
||||||
|
kbItems={[
|
||||||
|
{
|
||||||
|
label: '知识库',
|
||||||
|
path: '/knowledge'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: knowledge?.name || '知识库详情',
|
||||||
|
path: `/knowledge/${id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '设置'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
|
|||||||
@@ -236,7 +236,21 @@ function KnowledgeBaseTesting() {
|
|||||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
|
|
||||||
{/* 面包屑导航 */}
|
{/* 面包屑导航 */}
|
||||||
<KnowledgeBreadcrumbs knowledge={knowledgeDetail} />
|
<KnowledgeBreadcrumbs
|
||||||
|
kbItems={[
|
||||||
|
{
|
||||||
|
label: '知识库',
|
||||||
|
path: '/knowledge'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: knowledgeDetail?.name || '知识库详情',
|
||||||
|
path: `/knowledge/${id}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '测试'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<Box sx={{ mb: 3 }}>
|
<Box sx={{ mb: 3 }}>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import api from './api';
|
import api from './api';
|
||||||
import request, { post } from '@/utils/request';
|
import request, { post } from '@/utils/request';
|
||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
import type {
|
import type {
|
||||||
IFetchKnowledgeListRequestBody,
|
IFetchKnowledgeListRequestBody,
|
||||||
IFetchKnowledgeListRequestParams,
|
IFetchKnowledgeListRequestParams,
|
||||||
@@ -137,9 +138,10 @@ const knowledgeService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 获取文档文件
|
// 获取文档文件
|
||||||
getDocumentFile: (params: { doc_id: string }) => {
|
getDocumentFile: (params: { doc_id: string }, config?: AxiosRequestConfig) => {
|
||||||
return request.get(`${api.get_document_file}/${params.doc_id}`, {
|
return request.get(`${api.get_document_file}/${params.doc_id}`, {
|
||||||
responseType: 'blob'
|
responseType: 'blob',
|
||||||
|
...config
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -185,8 +187,8 @@ const knowledgeService = {
|
|||||||
return request.get(api.get_chunk, { params });
|
return request.get(api.get_chunk, { params });
|
||||||
},
|
},
|
||||||
|
|
||||||
// 切换分块状态
|
// 切换分块状态 available_int 是否启用,0:未启用,1:启用
|
||||||
switchChunk: (data: { chunk_ids: string[]; available_int: number }) => {
|
switchChunk: (data: { chunk_ids: string[]; available_int: number, doc_id: string }) => {
|
||||||
return post(api.switch_chunk, data);
|
return post(api.switch_chunk, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -136,13 +136,12 @@ request.interceptors.response.use(
|
|||||||
} else if (data?.code !== 0) {
|
} else if (data?.code !== 0) {
|
||||||
snackbar.error(data?.message);
|
snackbar.error(data?.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
// 处理网络错误
|
// 处理网络错误
|
||||||
if (error.message === FAILED_TO_FETCH || !error.response) {
|
if (error.message === FAILED_TO_FETCH || !error.response) {
|
||||||
notification.error(i18n.t('message.networkAnomaly'), i18n.t('message.networkAnomalyDescription'));
|
// notification.error(i18n.t('message.networkAnomaly'), i18n.t('message.networkAnomalyDescription'));
|
||||||
} else if (error.response) {
|
} else if (error.response) {
|
||||||
const { status, statusText } = error.response;
|
const { status, statusText } = error.response;
|
||||||
const errorText = RetcodeMessage[status as ResultCode] || statusText;
|
const errorText = RetcodeMessage[status as ResultCode] || statusText;
|
||||||
|
|||||||
Reference in New Issue
Block a user