diff --git a/web/public/auto-imports.d.ts b/web/public/auto-imports.d.ts index 62f88140..61cb2733 100644 --- a/web/public/auto-imports.d.ts +++ b/web/public/auto-imports.d.ts @@ -6,22 +6,31 @@ // biome-ignore lint: disable export {} declare global { + const Activity: typeof import('react').Activity + const Fragment: typeof import('react').Fragment const Link: typeof import('react-router-dom').Link const NavLink: typeof import('react-router-dom').NavLink const Navigate: typeof import('react-router-dom').Navigate const Outlet: typeof import('react-router-dom').Outlet const Route: typeof import('react-router-dom').Route const Routes: typeof import('react-router-dom').Routes + const Suspense: typeof import('react').Suspense + const cache: typeof import('react').cache + const cacheSignal: typeof import('react').cacheSignal + const createContext: typeof import('react').createContext const createRef: typeof import('react').createRef const forwardRef: typeof import('react').forwardRef const lazy: typeof import('react').lazy const memo: typeof import('react').memo const startTransition: typeof import('react').startTransition + const use: typeof import('react').use + const useActionState: typeof import('react').useActionState const useCallback: typeof import('react').useCallback const useContext: typeof import('react').useContext const useDebugValue: typeof import('react').useDebugValue const useDeferredValue: typeof import('react').useDeferredValue const useEffect: typeof import('react').useEffect + const useEffectEvent: typeof import('react').useEffectEvent const useHref: typeof import('react-router-dom').useHref const useId: typeof import('react').useId const useImperativeHandle: typeof import('react').useImperativeHandle @@ -33,6 +42,7 @@ declare global { const useMemo: typeof import('react').useMemo const useNavigate: typeof import('react-router-dom').useNavigate const useNavigationType: typeof import('react-router-dom').useNavigationType + const useOptimistic: typeof import('react').useOptimistic const useOutlet: typeof import('react-router-dom').useOutlet const useOutletContext: typeof import('react-router-dom').useOutletContext const useParams: typeof import('react-router-dom').useParams diff --git a/web/src/App.tsx b/web/src/App.tsx index c255f522..8e3140d9 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -27,12 +27,21 @@ import 'dayjs/locale/en' import 'dayjs/locale/zh-cn' import 'dayjs/plugin/timezone' import 'dayjs/plugin/utc' +import { cookieUtils } from './utils/request'; + + function App() { const { t } = useTranslation(); const { locale, language, timeZone } = useI18n() + useEffect(() => { + const authToken = cookieUtils.get('authToken') + if (!authToken && !window.location.hash.includes('#/login')) { + window.location.href = `/#/login`; + } + }, []) useEffect(() => { document.title = t('memoryBear') diff --git a/web/src/api/knowledgeBase.ts b/web/src/api/knowledgeBase.ts index 9791ee8d..60f374a2 100644 --- a/web/src/api/knowledgeBase.ts +++ b/web/src/api/knowledgeBase.ts @@ -200,7 +200,7 @@ export const deleteFile = async (id: string) => { // 获取文档列表 export const getDocumentList = async (query: PathQuery) => { - const response = await request.get(`${apiPrefix}/documents/${query.kb_id}/${query.parent_id}/documents`, query); + const response = await request.get(`${apiPrefix}/documents/${query.kb_id}/documents`, query); return response as KnowledgeBaseDocumentData[]; }; // 文档详情 @@ -213,6 +213,11 @@ export const createDocument = async (data: KnowledgeBaseDocumentData) => { const response = await request.post(`${apiPrefix}/documents/document`, data); return response as KnowledgeBaseDocumentData; }; +// 自定义文档上传并创建 +export const createDocumentAndUpload = async ( data: any, params: PathQuery) => { + const response = await request.post(`${apiPrefix}/files/customtext`, data, { params } ); + return response as any; +}; // 更新文档 export const updateDocument = async (id: string, data: KnowledgeBaseDocumentData) => { const response = await request.put(`${apiPrefix}/documents/${id}`, data); @@ -223,9 +228,9 @@ export const deleteDocument = async (id: string) => { const response = await request.delete(`${apiPrefix}/documents/${id}`); return response; }; -// 文档解析 -export const parseDocument = async (id: string) => { - const response = await request.post(`${apiPrefix}/documents/${id}/chunks`); +// 文档解析 / 分块 +export const parseDocument = async (id: string, data: any) => { + const response = await request.post(`${apiPrefix}/documents/${id}/chunks`, data); return response as any; }; // 文档分块预览 diff --git a/web/src/components/Header/index.tsx b/web/src/components/Header/index.tsx index 6ce5484e..9aeeab6b 100644 --- a/web/src/components/Header/index.tsx +++ b/web/src/components/Header/index.tsx @@ -3,6 +3,7 @@ import { Layout, Dropdown, Space, Breadcrumb } from 'antd'; import type { MenuProps, BreadcrumbProps } from 'antd'; import { UserOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; import { useUser } from '@/store/user'; import { useMenu } from '@/store/menu'; import styles from './index.module.css' @@ -12,12 +13,35 @@ const { Header } = Layout; const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { const { t } = useTranslation(); + const location = useLocation(); const settingModalRef = useRef(null) const userInfoModalRef = useRef(null) const { user, logout } = useUser(); const { allBreadcrumbs } = useMenu(); - const breadcrumbs = allBreadcrumbs[source] || []; + + // 根据当前路由动态选择面包屑源 + const getBreadcrumbSource = () => { + const pathname = location.pathname; + + // 知识库列表页面使用默认的 space 面包屑 + if (pathname === '/knowledge-base') { + return 'space'; + } + + // 知识库详情相关页面使用独立的面包屑 + if (pathname.includes('/knowledge-base/') && pathname !== '/knowledge-base') { + return 'space-detail'; + } + + // 其他页面使用传入的 source + return source; + }; + + const breadcrumbSource = getBreadcrumbSource(); + const breadcrumbs = allBreadcrumbs[breadcrumbSource] || []; + + // 处理退出登录 const handleLogout = () => { diff --git a/web/src/hooks/useBreadcrumbManager.ts b/web/src/hooks/useBreadcrumbManager.ts new file mode 100644 index 00000000..bd18f4f8 --- /dev/null +++ b/web/src/hooks/useBreadcrumbManager.ts @@ -0,0 +1,248 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useMenu } from '@/store/menu'; +import type { MenuItem } from '@/store/menu'; + +export interface BreadcrumbItem { + id: string; + name: string; + type?: 'knowledgeBase' | 'folder' | 'document'; +} + +export interface BreadcrumbPath { + knowledgeBaseFolderPath: BreadcrumbItem[]; // 知识库文件夹路径 + knowledgeBase?: BreadcrumbItem; // 知识库信息 + documentFolderPath: BreadcrumbItem[]; // 文档文件夹路径 + document?: BreadcrumbItem; // 文档信息 +} + +export interface BreadcrumbOptions { + onKnowledgeBaseMenuClick?: () => void; + onKnowledgeBaseFolderClick?: (folderId: string, folderPath: BreadcrumbItem[]) => void; + // 新增:区分面包屑类型 + breadcrumbType?: 'list' | 'detail'; +} + +export const useBreadcrumbManager = (options?: BreadcrumbOptions) => { + const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); + const navigate = useNavigate(); + + const updateBreadcrumbs = useCallback((breadcrumbPath: BreadcrumbPath) => { + const breadcrumbType = options?.breadcrumbType || 'list'; + + // 获取基础面包屑,对于详情页面,使用列表页面的基础面包屑作为起点 + const baseBreadcrumbs = breadcrumbType === 'list' + ? (allBreadcrumbs['space'] || []) + : (allBreadcrumbs['space'] || []); // 详情页面也从 space 获取基础面包屑 + + // 只保留知识库菜单项之前的面包屑 + const knowledgeBaseMenuIndex = baseBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); + const filteredBaseBreadcrumbs = knowledgeBaseMenuIndex >= 0 + ? baseBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1) + : baseBreadcrumbs; + + // 给"知识库管理"添加点击事件 + const breadcrumbsWithClick = filteredBaseBreadcrumbs.map((item) => { + if (item.path === '/knowledge-base') { + return { + ...item, + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + + if (options?.onKnowledgeBaseMenuClick) { + // 如果提供了回调函数,执行回调 + options.onKnowledgeBaseMenuClick(); + } else if (breadcrumbType === 'detail') { + // 知识库详情页面:没有回调函数时,返回到知识库列表页面 + navigate('/knowledge-base', { + state: { + resetToRoot: true, + } + }); + } + return false; + }, + }; + } + return item; + }); + + let customBreadcrumbs: MenuItem[] = [...breadcrumbsWithClick]; + + if (breadcrumbType === 'list') { + // 知识库列表页面:只显示知识库文件夹路径 + customBreadcrumbs = [ + ...breadcrumbsWithClick, + ...breadcrumbPath.knowledgeBaseFolderPath.map((folder, index) => ({ + id: 0, + parent: 0, + code: null, + label: folder.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + + // 如果有回调函数,直接调用回调函数来更新状态 + if (options?.onKnowledgeBaseFolderClick) { + options.onKnowledgeBaseFolderClick(folder.id, breadcrumbPath.knowledgeBaseFolderPath.slice(0, index + 1)); + } else { + // 否则使用导航(兜底逻辑) + navigate('/knowledge-base', { + state: { + navigateToFolder: folder.id, + folderPath: breadcrumbPath.knowledgeBaseFolderPath.slice(0, index + 1) + } + }); + } + return false; + }, + })), + ]; + } else { + // 知识库详情页面:显示知识库名称 + 文档文件夹路径 + 文档名称 + customBreadcrumbs = [ + ...breadcrumbsWithClick, + + // 添加知识库名称 + ...(breadcrumbPath.knowledgeBase ? [{ + id: 0, + parent: 0, + code: null, + label: breadcrumbPath.knowledgeBase.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + // 返回到知识库详情页的根目录 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath, + resetToRoot: true, // 添加重置到根目录的标志 + refresh: true, // 添加刷新标志 + timestamp: Date.now(), // 添加时间戳确保状态变化 + }; + navigate(`/knowledge-base/${breadcrumbPath.knowledgeBase!.id}/private`, { + state: navigationState, + replace: true // 使用 replace 避免历史记录堆积 + }); + return false; + }, + }] : []), + + // 添加文档文件夹路径 + ...breadcrumbPath.documentFolderPath.map((folder, index) => ({ + id: 0, + parent: 0, + code: null, + label: folder.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + // 返回到知识库详情页的对应文件夹 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath, + navigateToDocumentFolder: folder.id, + documentFolderPath: breadcrumbPath.documentFolderPath.slice(0, index + 1), + refresh: true, // 添加刷新标志 + timestamp: Date.now(), // 添加时间戳确保状态变化 + }; + navigate(`/knowledge-base/${breadcrumbPath.knowledgeBase!.id}/private`, { + state: navigationState, + replace: true // 使用 replace 避免历史记录堆积 + }); + return false; + }, + })), + + // 添加文档名称(如果存在) + ...(breadcrumbPath.document ? [{ + id: 0, + parent: 0, + code: null, + label: breadcrumbPath.document.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + // 文档名称不可点击 + }] : []), + ]; + } + + // 根据面包屑类型使用不同的键,实现独立的面包屑路径 + const breadcrumbKey = breadcrumbType === 'list' ? 'space' : 'space-detail'; + + + + setCustomBreadcrumbs(customBreadcrumbs, breadcrumbKey); + }, [setCustomBreadcrumbs, navigate, options?.breadcrumbType, options?.onKnowledgeBaseMenuClick, options?.onKnowledgeBaseFolderClick]); + + return { + updateBreadcrumbs, + }; +}; \ No newline at end of file diff --git a/web/src/hooks/useRouteGuard.ts b/web/src/hooks/useRouteGuard.ts index 23c31ec5..0bf6d485 100644 --- a/web/src/hooks/useRouteGuard.ts +++ b/web/src/hooks/useRouteGuard.ts @@ -11,8 +11,10 @@ export const checkAuthStatus = (): boolean => { // 递归检查路由是否存在于菜单数据中 export const checkRoutePermission = (menus: MenuItem[], currentPath: string): boolean => { - // 首页默认有权限 - if (currentPath === '/' || currentPath.includes('knowledge-detail')) return true; + // 首页和知识库相关页面默认有权限 + if (currentPath === '/' || currentPath.includes('knowledge-detail') || currentPath.includes('knowledge-base')) { + return true; + } for (const menu of menus) { // 检查当前菜单的path是否匹配 @@ -26,6 +28,7 @@ export const checkRoutePermission = (menus: MenuItem[], currentPath: string): bo } } } + return false; }; @@ -52,7 +55,7 @@ export const useRouteGuard = (source: 'space' | 'manage') => { const hasPermission = checkRoutePermission(menus, location.pathname); if (!hasPermission) { // 无权限访问该路由,重定向到无权限页面 - // navigate('/not-found', { replace: true }); + // navigate('/no-permission', { replace: true }); } } }, [navigate, location.pathname, location.search, location.hash, menus]); diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 41347c66..88698e22 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -479,11 +479,18 @@ export const en = { noDataSets: 'No datasets yet, click the button below or drag files to create.', createEmptyDataSet: '+ Empty Dataset', createImageDataSet: '+ Image Dataset', + createContent: 'Create Content', + title: 'Title', + content: 'Content', + pleaseEnterTitle: 'Please enter title', + pleaseEnterContent: 'Please enter content', + // createImageDataSet: '+ Image Dataset', dragFilesHere: 'Drag files here to upload', createImport: 'Create/Import', textDataSet: 'Text Dataset', imageDataSet: 'Image Dataset', blankDataset: 'Blank Dataset', + customTextDataset: 'Custom Text Dataset', text: 'Text', search: 'Search', image: 'Image', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 7aeaed03..7b68d8df 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -110,6 +110,11 @@ export const zh = { noDataSets: '暂无数据集,点击下方按钮或拖拽文件创建。', createEmptyDataSet: '+ 空白数据集', createImageDataSet: '+ 图片数据集', + createContent: '创建内容', + title: '标题', + content: '内容', + pleaseEnterTitle: '请输入标题', + pleaseEnterContent: '请输入内容', dragFilesHere: '拖拽文件到此处上传', downloadOriginal: '下载原始内容', createImport: '新建/导入', @@ -117,6 +122,7 @@ export const zh = { imageDataSet: '图片数据集', blankDataset: '空白数据集', emptyDataSet: '空白数据集', + customTextDataset: '自定义文本数据集', text: '文本', search: '搜索', image: '图片', diff --git a/web/src/store/menu.ts b/web/src/store/menu.ts index 7e725921..f6a66077 100644 --- a/web/src/store/menu.ts +++ b/web/src/store/menu.ts @@ -32,7 +32,7 @@ interface MenuState { allBreadcrumbs: Record<'space' | 'manage' | string, MenuItem[]>; loadMenus: (source: 'space' | 'manage') => void; updateBreadcrumbs: (keyPath: string[], source: 'space' | 'manage') => void; - setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: 'space' | 'manage') => void; + setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: string) => void; } const initBreadcrumbs = localStorage.getItem('breadcrumbs') || '[]' diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx index 92059e7e..d4a9084f 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/CreateDataset.tsx @@ -8,15 +8,14 @@ import type { UploadFileResponse,KnowledgeBaseDocumentData } from '@/views/Knowl import type { ColumnsType } from 'antd/es/table'; import UploadFiles from '@/components/Upload/UploadFiles'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; -import { uploadFile, getDocumentList, previewDocumentChunk, parseDocument, updateDocument, deleteDocument } from '@/api/knowledgeBase'; +import { uploadFile, getDocumentList, parseDocument, updateDocument, deleteDocument } from '@/api/knowledgeBase'; import exitIcon from '@/assets/images/knowledgeBase/exit.png'; -import { NoData } from '../components/noData'; -import noDataIcon from '@/assets/images/knowledgeBase/noData.png'; + import SliderInput from '@/components/SliderInput'; import DelimiterSelector from '../components/DelimiterSelector'; const { confirm } = Modal const { TextArea } = Input; -import styles from '../index.module.css'; + const style: React.CSSProperties = { display: 'flex', gap: 16, @@ -71,12 +70,11 @@ const CreateDataset = () => { const initialFileIds = locationState.fileIds ?? (locationState.fileId ? [locationState.fileId] : []); const [current, setCurrent] = useState(stepIndexMap[initialStepKey]); const tableRef = useRef(null); + + const [data, setData] = useState([]); - const [chunkData, setChunkData] = useState([]); - const [total, setTotal] = useState(0); const [rechunkFileIds, setRechunkFileIds] = useState(initialFileIds); - const [curSelectedFileId, setCurSelectedFileId] = useState(-1); - const [previewLoading, setPreviewLoading] = useState(false); + const [pollingLoading, setPollingLoading] = useState(false); const pollingTimerRef = useRef | null>(null); const [delimiter, setDelimiter] = useState(undefined); @@ -121,6 +119,7 @@ const CreateDataset = () => { layout_recognize:'DeepDOC', delimiter: delimiter, chunk_token_num: blockSize, + auto_question: processingMethod === 'directBlock' ? 0 : 1, } } updateDocument(id, params) @@ -145,7 +144,7 @@ const CreateDataset = () => { }); return; } - debugger + // 显示确认弹框 confirm({ @@ -168,7 +167,7 @@ const CreateDataset = () => { const startProcessing = (autoReturnToList: boolean) => { // 触发文档解析 rechunkFileIds.map((id) => { - parseDocument(id); + parseDocument(id, {}); }); // 开启 loading @@ -276,21 +275,7 @@ const CreateDataset = () => { onError?.(error as Error); }); }; - // 点击文件 预览分块 - const handlePreview = async(item: KnowledgeBaseDocumentData, index: number) => { - setCurSelectedFileId(index); - setPreviewLoading(true); - try{ - const res = await previewDocumentChunk(knowledgeBaseId ?? '', item.id ?? ''); - setChunkData(res.items || []); - setTotal(res.page.total || 0); - console.log('res', res); - }catch(error) { - console.log('error', error); - } finally { - setPreviewLoading(false); - } - } + // 轮询检查文档处理状态 // autoReturn: 是否在所有文档完成时自动返回列表页 @@ -346,6 +331,8 @@ const CreateDataset = () => { state: { refresh: true, timestamp: Date.now(), // 添加时间戳确保每次都是新的 state + // 保持返回到原来的文档文件夹位置 + navigateToDocumentFolder: parentId !== knowledgeBaseId ? parentId : undefined, }, }); } else { @@ -565,8 +552,8 @@ const CreateDataset = () => { {rechunkFileIds.length > 0 ? ( { const navigate = useNavigate(); const { knowledgeBaseId } = useParams<{ knowledgeBaseId: string }>(); const location = useLocation(); - const { documentId, parentId: locationParentId } = location.state as { documentId: string; parentId?: string }; + const { updateBreadcrumbs } = useBreadcrumbManager({ + breadcrumbType: 'detail' + }); + const { + documentId, + parentId: locationParentId, + breadcrumbPath + } = location.state as { + documentId: string; + parentId?: string; + breadcrumbPath?: BreadcrumbPath; + }; const [loading, setLoading] = useState(false); const [document, setDocument] = useState(null); const [chunkList, setChunkList] = useState([]); @@ -44,6 +56,13 @@ const DocumentDetails: FC = () => { } }, [documentId]); + // 更新面包屑 + useEffect(() => { + if (breadcrumbPath) { + updateBreadcrumbs(breadcrumbPath); + } + }, [breadcrumbPath, updateBreadcrumbs]); + // 当文档加载完成且 progress === 1 时,加载分块列表 useEffect(() => { if (document && document.progress === 1 && !isManualRefreshRef.current) { @@ -179,7 +198,18 @@ const DocumentDetails: FC = () => { }; const handleBack = () => { - if (knowledgeBaseId) { + if (knowledgeBaseId && breadcrumbPath) { + // 返回到知识库详情页,并传递面包屑信息以恢复状态 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath, + navigateToDocumentFolder: locationParentId, + documentFolderPath: breadcrumbPath.documentFolderPath, + timestamp: Date.now(), // 添加时间戳确保状态变化 + }; + navigate(`/knowledge-base/${knowledgeBaseId}/private`, { state: navigationState }); + } else if (knowledgeBaseId) { + // 降级处理:直接跳转到知识库详情页 navigate(`/knowledge-base/${knowledgeBaseId}/private`); } }; diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx index 7c43b79f..ed909515 100644 --- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx +++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState, useRef, type FC } from 'react'; +import { useEffect, useState, useRef, useCallback, type FC } from 'react'; import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Switch, Button, Dropdown, Space, Modal, message } from 'antd'; @@ -12,26 +12,29 @@ import { MoreOutlined } from '@ant-design/icons'; import folderIcon from '@/assets/images/knowledgeBase/folder.png'; import textIcon from '@/assets/images/knowledgeBase/text.png'; import editIcon from '@/assets/images/knowledgeBase/edit.png'; +import blankIcon from '@/assets/images/knowledgeBase/blankDocument.png'; import { getKnowledgeBaseDetail, deleteDocument, downloadFile, updateKnowledgeBase } from '@/api/knowledgeBase'; -import type { - CreateModalRef, - KnowledgeBaseListItem, - RecallTestDrawerRef, - CreateFolderModalRef, - CreateImageModalRef, - ShareModalRef, - CreateDatasetModalRef,FolderFormData, - KnowledgeBaseDocumentData +import { + type CreateModalRef, + type KnowledgeBaseListItem, + type RecallTestDrawerRef, + type CreateFolderModalRef, + type CreateSetModalRef, + type ShareModalRef, + type CreateDatasetModalRef,type FolderFormData, + type KnowledgeBaseDocumentData, } from '@/views/KnowledgeBase/types'; import RecallTestDrawer from '../components/RecallTestDrawer'; import CreateFolderModal from '../components/CreateFolderModal'; +import CreateContentModal from '../components/CreateContentModal'; import CreateModal from '../components/CreateModal'; import ShareModal from '../components/ShareModal'; import CreateDatasetModal from '../components/CreateDatasetModal'; import CreateImageDataset from '../components/CreateImageDataset'; import FolderTree, { type TreeNodeData } from '../components/FolderTree'; import { formatDateTime } from '@/utils/format'; -import { useMenu } from '@/store/menu'; + +import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager'; import './Private.css' const { confirm } = Modal // 树节点数据类型 @@ -48,7 +51,8 @@ const Private: FC = () => { const [tableApi, setTableApi] = useState(undefined); const recallTestDrawerRef = useRef(null); const createFolderModalRef = useRef(null); - const createImageDataset = useRef(null) + const createImageDataset = useRef(null) + const createContentModalRef = useRef(null); const [knowledgeBase, setKnowledgeBase] = useState(null); const [folder, setFolder] = useState({ kb_id:knowledgeBaseId ?? '', @@ -56,47 +60,47 @@ const Private: FC = () => { }); const [query, setQuery] = useState>({ orderby: 'created_at', - desc: true, + desc: true }); const modalRef = useRef(null) const shareModalRef = useRef(null); const datasetModalRef = useRef(null); const [folderTreeRefreshKey, setFolderTreeRefreshKey] = useState(0); - const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); - const [folderPath, setFolderPath] = useState>([]); + const [autoExpandPath, setAutoExpandPath] = useState>([]); + + const { updateBreadcrumbs } = useBreadcrumbManager({ + breadcrumbType: 'detail', + // 不提供 onKnowledgeBaseMenuClick,让它使用默认的导航行为(返回列表页面) + onKnowledgeBaseFolderClick: useCallback((folderId: string, folderPath: Array<{ id: string; name: string }>) => { + // 点击文件夹面包屑时,导航到对应文件夹 + setParentId(folderId); + setFolderPath(folderPath); + setSelectedKeys([folderId]); + setFolder({ + kb_id: knowledgeBaseId ?? '', + parent_id: folderId + }); + + // 确保query对象发生变化,触发表格刷新 + setQuery({ + orderby: 'created_at', + desc: true, + parent_id: folderId, + _timestamp: Date.now() + }); + + // 确保API URL正确设置 + setTableApi(`/documents/${knowledgeBaseId}/documents`); + + // 手动触发表格刷新,确保数据更新 + setTimeout(() => { + tableRef.current?.loadData(); + }, 100); + }, [knowledgeBaseId]) + }); + const [folderPath, setFolderPath] = useState([]); const [selectedKeys, setSelectedKeys] = useState([]); - useEffect(() => { - if (knowledgeBaseId) { - let url = `/documents/${knowledgeBaseId}/${parentId}/documents`; - setTableApi(url); - fetchKnowledgeBaseDetail(knowledgeBaseId); - } - }, [knowledgeBaseId]); - - // 更新面包屑 - useEffect(() => { - if (knowledgeBase) { - updateBreadcrumbs(); - } - }, [knowledgeBase, folderPath]); - - // 监听 tableApi 变化,自动刷新表格数据 - useEffect(() => { - if (tableApi) { - tableRef.current?.loadData(); - } - }, [tableApi]); - - // 监听 location state 变化,如果有 refresh 标志则刷新列表 - useEffect(() => { - const state = location.state as { refresh?: boolean; timestamp?: number } | null; - if (state?.refresh) { - tableRef.current?.loadData(); - // 清除 state,避免重复刷新 - navigate(location.pathname, { replace: true, state: {} }); - } - }, [location.state]); - + const [knowledgeBaseFolderPath, setKnowledgeBaseFolderPath] = useState([]); const fetchKnowledgeBaseDetail = async (id: string) => { setLoading(true); try { @@ -109,110 +113,160 @@ const Private: FC = () => { } }; - // 更新面包屑,包含知识库名称和文件夹路径 - const updateBreadcrumbs = () => { - if (!knowledgeBase) return; - - const baseBreadcrumbs = allBreadcrumbs['space'] || []; - // 只保留知识库菜单项之前的面包屑 - const knowledgeBaseMenuIndex = baseBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); - const filteredBaseBreadcrumbs = knowledgeBaseMenuIndex >= 0 - ? baseBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1) - : baseBreadcrumbs; - - const customBreadcrumbs = [ - ...filteredBaseBreadcrumbs, - { - id: 0, - parent: 0, - code: null, - label: knowledgeBase.name, - i18nKey: null, - path: null, - enable: true, - display: true, - level: 0, - sort: 0, - icon: null, - iconActive: null, - menuDesc: null, - deleted: null, - updateTime: 0, - new_: null, - keepAlive: false, - master: null, - disposable: false, - appSystem: null, - subs: [], - onClick: (e?: React.MouseEvent) => { - // 阻止默认行为和事件冒泡 - e?.preventDefault(); - e?.stopPropagation(); - // 点击知识库名称,回到根目录 - setParentId(knowledgeBaseId); - setFolder({ - kb_id: knowledgeBaseId ?? '', - parent_id: knowledgeBaseId ?? '' - }); - setTableApi(`/documents/${knowledgeBaseId}/${knowledgeBaseId}/documents`); - setFolderPath([]); - setSelectedKeys([knowledgeBaseId ?? '']); - return false; - }, - }, - ...folderPath.map((folder, index) => ({ - id: 0, - parent: 0, - code: null, - label: folder.name, - i18nKey: null, - path: null, - enable: true, - display: true, - level: 0, - sort: 0, - icon: null, - iconActive: null, - menuDesc: null, - deleted: null, - updateTime: 0, - new_: null, - keepAlive: false, - master: null, - disposable: false, - appSystem: null, - subs: [], - onClick: (e?: React.MouseEvent) => { - // 阻止默认行为和事件冒泡 - e?.preventDefault(); - e?.stopPropagation(); - // 点击文件夹,回到该文件夹层级 - setParentId(folder.id); - setFolder({ - kb_id: knowledgeBaseId ?? '', - parent_id: folder.id - }); - setTableApi(`/documents/${knowledgeBaseId}/${folder.id}/documents`); - // 更新文件夹路径,只保留到当前点击的文件夹 - setFolderPath(folderPath.slice(0, index + 1)); - setSelectedKeys([folder.id]); - return false; - }, - })), - ]; + useEffect(() => { + if (knowledgeBaseId) { + let url = `/documents/${knowledgeBaseId}/documents`; + setTableApi(url); + fetchKnowledgeBaseDetail(knowledgeBaseId); + } + }, [knowledgeBaseId]); - setCustomBreadcrumbs(customBreadcrumbs, 'space'); - }; + // 更新面包屑 + useEffect(() => { + if (knowledgeBase) { + updateBreadcrumbs({ + knowledgeBaseFolderPath, + knowledgeBase: { + id: knowledgeBase.id, + name: knowledgeBase.name, + type: 'knowledgeBase' + }, + documentFolderPath: folderPath, + }); + } + }, [knowledgeBase, knowledgeBaseFolderPath, folderPath, updateBreadcrumbs]); + + // 监听 tableApi 变化,自动刷新表格数据 + useEffect(() => { + if (tableApi) { + tableRef.current?.loadData(); + } + }, [tableApi]); + + // 监听 query 变化,确保表格数据更新 + useEffect(() => { + if (tableApi && query._timestamp) { + // 当 query 中有 _timestamp 时,说明是通过面包屑或其他方式触发的更新 + tableRef.current?.loadData(); + } + }, [query._timestamp, tableApi]); + + // 监听 location state 变化 + useEffect(() => { + const state = location.state as { + refresh?: boolean; + timestamp?: number; + fromKnowledgeBaseList?: boolean; + knowledgeBaseFolderPath?: BreadcrumbItem[]; + parentId?: string; + navigateToDocumentFolder?: string; + documentFolderPath?: BreadcrumbItem[]; + resetToRoot?: boolean; + } | null; + + if (state?.refresh) { + tableRef.current?.loadData(); + // 清除 state,避免重复刷新 + navigate(location.pathname, { replace: true, state: {} }); + } + + // 如果是从知识库列表页跳转过来的,设置知识库文件夹路径 + if (state?.fromKnowledgeBaseList && state?.knowledgeBaseFolderPath) { + setKnowledgeBaseFolderPath(state.knowledgeBaseFolderPath); + } + + // 如果需要重置到根目录(回到初始状态) + if (state?.resetToRoot) { + // 重置所有状态到初始状态,和页面初始化保持一致 + setParentId(knowledgeBaseId); + setFolderPath([]); + setSelectedKeys([]); + setFolder({ + kb_id: knowledgeBaseId ?? '', + parent_id: knowledgeBaseId ?? '' + }); + setQuery({ + orderby: 'created_at', + desc: true, + _timestamp: Date.now() // 添加时间戳确保query对象发生变化,触发API调用 + }); + + // 重新设置API URL + const rootUrl = `/documents/${knowledgeBaseId}/documents`; + setTableApi(rootUrl); + + // 清除自动展开路径 + setAutoExpandPath([]); + + // 刷新文件夹树(简单的刷新,不需要复杂的重置逻辑) + setFolderTreeRefreshKey((prev) => prev + 1); + + // 清除 state,避免重复处理 + navigate(location.pathname, { replace: true, state: {} }); + } + + // 如果是从文档详情页返回,恢复文档文件夹路径 + if (state?.navigateToDocumentFolder && state?.documentFolderPath) { + setFolderPath(state.documentFolderPath); + setParentId(state.navigateToDocumentFolder); + setFolder({ + kb_id: knowledgeBaseId ?? '', + parent_id: state.navigateToDocumentFolder + }); + setQuery(prevQuery => ({ + ...prevQuery, + parent_id: state.navigateToDocumentFolder, + _timestamp: Date.now() + })); + setTableApi(`/documents/${knowledgeBaseId}/documents`); + setSelectedKeys([state.navigateToDocumentFolder]); + + // 设置自动展开路径,让FolderTree自动展开到对应位置 + setAutoExpandPath(state.documentFolderPath); + + // 手动触发表格刷新 + setTimeout(() => { + tableRef.current?.loadData(); + }, 100); + + // 清除自动展开路径,避免重复触发(延迟清除,确保FolderTree处理完成) + setTimeout(() => { + setAutoExpandPath([]); + }, 2000); + } + }, [location.state, knowledgeBaseId, navigate, location.pathname]); // 处理树节点选择 const onSelect = (keys: React.Key[]) => { - if (!keys.length) return; + if (!keys.length) { + // 如果没有选中任何节点,回到根目录(初始状态) + setParentId(knowledgeBaseId); + setFolder({ + kb_id: knowledgeBaseId ?? '', + parent_id: knowledgeBaseId ?? '' + }); + setQuery({ + orderby: 'created_at', + desc: true, + _timestamp: Date.now() // 添加时间戳确保query对象发生变化 + }); + setSelectedKeys([]); + return; + } + if (!folder) return; + const f = { ...folder, parent_id: String(keys[0]), } - let url = `/documents/${knowledgeBaseId}/${String(keys[0])}/documents`; + setQuery({ + ...query, + parent_id: String(keys[0]), + _timestamp: Date.now() // 添加时间戳确保query对象发生变化 + }) + let url = `/documents/${knowledgeBaseId}/documents`; + setTableApi(url); setParentId(String(keys[0])) setFolder(f) @@ -253,6 +307,15 @@ const Private: FC = () => { datasetModalRef?.current?.handleOpen(knowledgeBase?.id,folder?.parent_id ?? knowledgeBase?.id ?? ''); }, }, + { + key: '8', + icon: Custome Text, + label: t('knowledgeBase.customTextDataset'), + onClick: () => { + createContentModalRef?.current?.handleOpen(knowledgeBase?.id ?? '', folder?.parent_id ?? knowledgeBase?.id ?? ''); + // handleCreate('folder'); // 传入 type: 'folder' + }, + }, // 暂时未实现 // { // key: '3', @@ -413,6 +476,21 @@ const Private: FC = () => { state: { documentId: document.id, parentId: parentId ?? knowledgeBaseId, + // 传递面包屑信息 + breadcrumbPath: { + knowledgeBaseFolderPath, + knowledgeBase: { + id: knowledgeBase?.id || knowledgeBaseId, + name: knowledgeBase?.name || '', + type: 'knowledgeBase' + }, + documentFolderPath: folderPath, + document: { + id: document.id, + name: document.file_name || '', + type: 'document' + } + } }, }); } @@ -486,7 +564,9 @@ const Private: FC = () => { } const refreshDirectoryTree = async () => { // 先刷新知识库详情,确保数据是最新的 - await fetchKnowledgeBaseDetail(knowledgeBase.id); + if (knowledgeBase?.id) { + await fetchKnowledgeBaseDetail(knowledgeBase.id); + } // 添加短暂延迟,确保后端数据已经完全更新 await new Promise(resolve => setTimeout(resolve, 300)); // 然后刷新文件夹树 @@ -501,6 +581,7 @@ const Private: FC = () => { } const handleRootTreeLoad = (nodes: TreeNodeData[] | null) => { if (!nodes || nodes.length === 0) { + // 如果没有节点,设置folder为null(这会隐藏FolderTree) setFolder(null); } else { // 如果有节点且 folder 为 null,重新设置 folder @@ -524,6 +605,7 @@ const Private: FC = () => { } const handleRefreshTable = () => { + debugger // 刷新表格数据 tableRef.current?.loadData(); } @@ -545,6 +627,7 @@ const Private: FC = () => { onRootLoad={handleRootTreeLoad} onFolderPathChange={handleFolderPathChange} selectedKeys={selectedKeys} + autoExpandPath={autoExpandPath} /> )} @@ -601,6 +684,10 @@ const Private: FC = () => { ref={createFolderModalRef} refreshTable={refreshDirectoryTree} /> + { const { t } = useTranslation(); const params = useParams<{ knowledgeBaseId: string }>(); + const location = useLocation(); const knowledgeBaseId = params.knowledgeBaseId; const [loading, setLoading] = useState(false); const [knowledgeBase, setKnowledgeBase] = useState(null); const recallTestRef = useRef(null); const [infoItems, setInfoItems] = useState([]); - const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); + const [knowledgeBaseFolderPath, setKnowledgeBaseFolderPath] = useState([]); + + const { updateBreadcrumbs } = useBreadcrumbManager({ + breadcrumbType: 'detail' + }); useEffect(() => { console.log('Share.tsx - useParams result:', params); console.log('Share.tsx - knowledgeBaseId:', knowledgeBaseId); @@ -46,9 +51,30 @@ const Share: FC = () => { // 更新面包屑 useEffect(() => { if (knowledgeBase) { - updateBreadcrumbs(); + updateBreadcrumbs({ + knowledgeBaseFolderPath, + knowledgeBase: { + id: knowledgeBase.id, + name: knowledgeBase.name, + type: 'knowledgeBase' + }, + documentFolderPath: [], + }); } - }, [knowledgeBase]); + }, [knowledgeBase, knowledgeBaseFolderPath, updateBreadcrumbs]); + + // 监听 location state 变化 + useEffect(() => { + const state = location.state as { + fromKnowledgeBaseList?: boolean; + knowledgeBaseFolderPath?: BreadcrumbItem[]; + } | null; + + // 如果是从知识库列表页跳转过来的,设置知识库文件夹路径 + if (state?.fromKnowledgeBaseList && state?.knowledgeBaseFolderPath) { + setKnowledgeBaseFolderPath(state.knowledgeBaseFolderPath); + } + }, [location.state]); const formatInfoItems = (data: KnowledgeBaseListItem): InfoItem[] => { const items: InfoItem[] = [ { @@ -112,46 +138,7 @@ const Share: FC = () => { }); }; - // 更新面包屑,包含知识库名称 - const updateBreadcrumbs = () => { - if (!knowledgeBase) return; - - const baseBreadcrumbs = allBreadcrumbs['space'] || []; - // 只保留知识库菜单项之前的面包屑 - const knowledgeBaseMenuIndex = baseBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); - const filteredBaseBreadcrumbs = knowledgeBaseMenuIndex >= 0 - ? baseBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1) - : baseBreadcrumbs; - - const customBreadcrumbs = [ - ...filteredBaseBreadcrumbs, - { - id: 0, - parent: 0, - code: null, - label: knowledgeBase.name, - i18nKey: null, - path: null, - enable: true, - display: true, - level: 0, - sort: 0, - icon: null, - iconActive: null, - menuDesc: null, - deleted: null, - updateTime: 0, - new_: null, - keepAlive: false, - master: null, - disposable: false, - appSystem: null, - subs: [], - }, - ]; - setCustomBreadcrumbs(customBreadcrumbs, 'space'); - }; // const handleBack = () => { // navigate('/knowledge-base'); diff --git a/web/src/views/KnowledgeBase/components/CreateContentModal.tsx b/web/src/views/KnowledgeBase/components/CreateContentModal.tsx new file mode 100644 index 00000000..63292dec --- /dev/null +++ b/web/src/views/KnowledgeBase/components/CreateContentModal.tsx @@ -0,0 +1,117 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { Form, Input } from 'antd'; +import { useTranslation } from 'react-i18next'; +import RbModal from '@/components/RbModal'; +import { createDocumentAndUpload } from '@/api/knowledgeBase' +import type { CreateSetModalRef,CreateSetMoealRefProps } from '../types' +interface ContentFormData { + title: string; + content: string; +} + +const CreateContentModal = forwardRef( + ({ refreshTable }, ref) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [kbId, setKbId] = useState(''); + const [parentId, setParentId] = useState(''); + + const handleClose = () => { + form.resetFields(); + setLoading(false); + setVisible(false); + setKbId(''); + setParentId(''); + }; + + const handleOpen = (kb_id: string, parent_id: string) => { + setKbId(kb_id); + setParentId(parent_id); + form.resetFields(); + setVisible(true); + }; + + const handleSave = async () => { + try { + const values = await form.validateFields(); + setLoading(true); + + // TODO: 这里需要调用相应的API来保存内容 + const params = { + // ...values, + kb_id: kbId, + parent_id: parentId, + }; + + + const response = await createDocumentAndUpload(values, params) + if(response){ + handleChunking(response.kb_id,parentId,response.id) + } + handleClose(); + } catch (err) { + console.error('创建内容失败:', err); + } finally { + setLoading(false); + } + }; + const handleChunking = (kb_id: string, parent_id: string, file_id: string) => { + if (!kb_id) return; + const targetFileId = file_id + navigate(`/knowledge-base/${kb_id}/create-dataset`, { + state: { + source: 'local', + knowledgeBaseId: kb_id, + parentId: parent_id ?? kb_id, + startStep: 'parameterSettings', + fileId: targetFileId, + }, + }); + } + useImperativeHandle(ref, () => ({ + handleOpen, + })); + + return ( + +
+ + + + + + + + +
+ ); + } +); + +export default CreateContentModal; \ No newline at end of file diff --git a/web/src/views/KnowledgeBase/components/CreateContentModalExample.tsx b/web/src/views/KnowledgeBase/components/CreateContentModalExample.tsx new file mode 100644 index 00000000..7434e559 --- /dev/null +++ b/web/src/views/KnowledgeBase/components/CreateContentModalExample.tsx @@ -0,0 +1,34 @@ +import { useRef } from 'react'; +import { Button } from 'antd'; +import CreateContentModal from './CreateContentModal'; +import type { CreateContentModalRef } from '../types'; + +// 使用示例组件 +const CreateContentModalExample = () => { + const createContentModalRef = useRef(null); + + const handleOpenModal = () => { + // 打开弹窗,传入知识库ID和父级ID + createContentModalRef.current?.handleOpen('kb_123', 'parent_456'); + }; + + const handleRefreshTable = () => { + console.log('刷新表格数据'); + // 这里可以添加刷新表格的逻辑 + }; + + return ( +
+ + + +
+ ); +}; + +export default CreateContentModalExample; \ No newline at end of file diff --git a/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx b/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx index 4f40da7f..8f57b009 100644 --- a/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx +++ b/web/src/views/KnowledgeBase/components/CreateImageDataset.tsx @@ -2,7 +2,7 @@ import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; import { Form, Input } from 'antd'; import { useTranslation } from 'react-i18next'; import type { UploadFile } from 'antd'; -import type { CreateImageModalRef, CreateImageMoealRefProps,UploadFileResponse } from '@/views/KnowledgeBase/types'; +import type { CreateSetModalRef, CreateSetMoealRefProps, UploadFileResponse } from '@/views/KnowledgeBase/types'; import type { UploadRequestOption } from 'rc-upload/lib/interface'; import RbModal from '@/components/RbModal'; import UploadFiles from '@/components/Upload/UploadFiles'; @@ -13,7 +13,7 @@ interface ImageDatasetFormData { images: UploadFile[]; } -const CreateImageDataset = forwardRef( +const CreateImageDataset = forwardRef( ({ refreshTable }, ref) => { const { t } = useTranslation(); const [visible, setVisible] = useState(false); diff --git a/web/src/views/KnowledgeBase/components/FolderTree.tsx b/web/src/views/KnowledgeBase/components/FolderTree.tsx index 4a5a288c..299e4cf3 100644 --- a/web/src/views/KnowledgeBase/components/FolderTree.tsx +++ b/web/src/views/KnowledgeBase/components/FolderTree.tsx @@ -60,6 +60,8 @@ interface FolderTreeProps { onRootLoad?: (nodes: TreeNodeData[] | null) => void; onFolderPathChange?: (path: Array<{ id: string; name: string }>) => void; selectedKeys?: React.Key[]; + // 新增:自动展开到指定路径 + autoExpandPath?: Array<{ id: string; name: string }>; } const renderIcon = (icon?: string) => { @@ -275,8 +277,11 @@ const FolderTree: FC = ({ onRootLoad, onFolderPathChange, selectedKeys, + autoExpandPath, }) => { const [treeData, setTreeData] = useState([]); + const [expandedKeys, setExpandedKeys] = useState([]); + const [autoExpandInProgress, setAutoExpandInProgress] = useState(false); // 更新树节点数据的辅助函数 const updateTreeData = (nodes: TreeNodeData[], key: Key, children: TreeNodeData[]): TreeNodeData[] => { @@ -370,6 +375,109 @@ const FolderTree: FC = ({ return null; }; + // 查找节点的辅助函数 + const findNodeInTree = (nodes: TreeNodeData[], key: string): TreeNodeData | null => { + for (const node of nodes) { + if (String(node.key) === key) { + return node; + } + if (node.children) { + const found = findNodeInTree(node.children, key); + if (found) return found; + } + } + return null; + }; + + // 渐进式自动展开到指定路径 + useEffect(() => { + if (!autoExpandPath || autoExpandPath.length === 0 || autoExpandInProgress || treeData.length === 0) { + return; + } + + const expandToPath = async () => { + setAutoExpandInProgress(true); + + try { + const keysToExpand: React.Key[] = []; + let currentTreeData = treeData; + + // 逐级展开,从第一级开始(跳过根节点,因为根节点已经加载) + for (let i = 0; i < autoExpandPath.length - 1; i++) { + const nodeKey = autoExpandPath[i].id; + keysToExpand.push(nodeKey); + + // 查找当前节点 + const targetNode = findNodeInTree(currentTreeData, nodeKey); + + if (targetNode && targetNode.children === undefined) { + // 如果子节点未加载,先加载 + try { + console.log(`自动展开:加载节点 ${nodeKey} 的子节点`); + const children = await buildTreeNodes(knowledgeBaseId, nodeKey); + + // 更新树数据 + setTreeData((prevData) => { + const newData = updateTreeData(prevData, nodeKey, children); + currentTreeData = newData; // 更新当前引用 + return newData; + }); + + // 等待状态更新完成 + await new Promise(resolve => setTimeout(resolve, 150)); + + } catch (error) { + console.error(`自动展开时加载节点 ${nodeKey} 失败:`, error); + // 加载失败时停止展开 + break; + } + } + } + + // 设置展开的节点 + setExpandedKeys(keysToExpand); + + // 选中最后一个节点(目标文件夹) + const targetKey = autoExpandPath[autoExpandPath.length - 1]?.id; + if (targetKey) { + console.log(`自动展开:选中目标节点 ${targetKey}`); + // 延迟选中,确保展开动画完成 + setTimeout(() => { + if (onSelect) { + onSelect([targetKey], { + selected: true, + selectedNodes: [], + node: {} as any, + event: 'select', + nativeEvent: new MouseEvent('click') + }); + } + }, 200); + } + + } catch (error) { + console.error('自动展开路径失败:', error); + } finally { + // 延迟重置标志,确保展开过程完全完成 + setTimeout(() => { + setAutoExpandInProgress(false); + }, 500); + } + }; + + // 延迟执行,确保树数据已经加载完成 + const timer = setTimeout(expandToPath, 300); + return () => clearTimeout(timer); + }, [autoExpandPath, treeData.length, knowledgeBaseId, onSelect, autoExpandInProgress]); + + // 处理展开事件 + const handleExpand: TreeProps['onExpand'] = (expandedKeys, info) => { + setExpandedKeys(expandedKeys); + if (onExpand) { + onExpand(expandedKeys, info); + } + }; + // 处理选择事件,计算并传递路径 const handleSelect: TreeProps['onSelect'] = (selectedKeys, info) => { if (selectedKeys.length > 0) { @@ -391,11 +499,13 @@ const FolderTree: FC = ({ return ( ; @@ -28,6 +29,7 @@ type ModelMenuInfo = { const KnowledgeBaseManagement: FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); + const location = useLocation(); const [loading, setLoading] = useState(false); const [data, setData] = useState([]) const [page, setPage] = useState(1) @@ -42,10 +44,29 @@ const KnowledgeBaseManagement: FC = () => { const modelListCache = useRef>({}); const modalRef = useRef(null) const [messageApi, contextHolder] = message.useMessage(); + const processedStateRef = useRef(null); - // 使用 menu store 管理面包屑 - const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); - const [folderPath, setFolderPath] = useState>([]); + // 使用面包屑管理 Hook + const { updateBreadcrumbs } = useBreadcrumbManager({ + breadcrumbType: 'list', + onKnowledgeBaseMenuClick: useCallback(() => { + // 返回根目录 + setFolderPath([]); + setQuery((prev) => ({ + ...prev, + parent_id: undefined, + })); + }, []), + onKnowledgeBaseFolderClick: useCallback((folderId: string, folderPath: Array<{ id: string; name: string }>) => { + // 直接更新文件夹路径和查询状态 + setFolderPath(folderPath); + setQuery((prev) => ({ + ...prev, + parent_id: folderId, + })); + }, []) + }); + const [folderPath, setFolderPath] = useState([]); // 生成下拉菜单项(根据当前 item) @@ -134,7 +155,7 @@ const KnowledgeBaseManagement: FC = () => { handleCreate(type); }, })); - }, [knowledgeBaseTypes, t, folderPath, query]); + }, [knowledgeBaseTypes, t]); const typeToFieldKey = (type: string) => { const normalized = (type || '').toLowerCase(); switch (normalized) { @@ -371,90 +392,72 @@ const KnowledgeBaseManagement: FC = () => { })); return; } - // 根据权限类型跳转到不同的详情页 - if (knowledgeBase.permission_id === 'Private' || knowledgeBase.permission_id === 'private') { - navigate(`/knowledge-base/${knowledgeBase.id}/private`) + // 跳转时传递当前的文件夹路径信息 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: folderPath, + parentId: query.parent_id, + timestamp: Date.now(), // 添加时间戳确保每次跳转状态都不同 + }; + const targetPath = knowledgeBase.permission_id === 'Private' || knowledgeBase.permission_id === 'private' + ? `/knowledge-base/${knowledgeBase.id}/private` + : `/knowledge-base/${knowledgeBase.id}/share`; + + // 检查是否是相同路径跳转 + const currentPath = location.pathname; + + if (currentPath === targetPath) { + // 如果是相同路径,使用replace并强制刷新状态 + navigate(targetPath, { + state: navigationState, + replace: true + }); } else { - navigate(`/knowledge-base/${knowledgeBase.id}/share`) + // 不同路径,正常跳转 + navigate(targetPath, { state: navigationState }); } } - // 更新面包屑的函数 - const updateBreadcrumbs = () => { - const baseBreadcrumbs = allBreadcrumbs['space'] || []; - // 只保留知识库菜单项之前的面包屑 - const knowledgeBaseMenuIndex = baseBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); - const filteredBaseBreadcrumbs = knowledgeBaseMenuIndex >= 0 - ? baseBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1) - : baseBreadcrumbs; - - // 给"知识库管理"添加点击事件,返回根目录 - const breadcrumbsWithClick = filteredBaseBreadcrumbs.map((item) => { - if (item.path === '/knowledge-base') { - return { - ...item, - onClick: (e?: React.MouseEvent) => { - e?.preventDefault(); - e?.stopPropagation(); - // 返回根目录 - setFolderPath([]); - setQuery((prev) => ({ - ...prev, - parent_id: undefined, - })); - return false; - }, - }; - } - return item; - }); - - const customBreadcrumbs = [ - ...breadcrumbsWithClick, - ...folderPath.map((folder, index) => ({ - id: 0, - parent: 0, - code: null, - label: folder.name, - i18nKey: null, - path: null, - enable: true, - display: true, - level: 0, - sort: 0, - icon: null, - iconActive: null, - menuDesc: null, - deleted: null, - updateTime: 0, - new_: null, - keepAlive: false, - master: null, - disposable: false, - appSystem: null, - subs: [], - onClick: (e?: React.MouseEvent) => { - e?.preventDefault(); - e?.stopPropagation(); - // 点击文件夹,回到该文件夹层级 - const newFolderPath = folderPath.slice(0, index + 1); - setFolderPath(newFolderPath); - setQuery((prev) => ({ - ...prev, - parent_id: folder.id, - })); - return false; - }, - })), - ]; - - setCustomBreadcrumbs(customBreadcrumbs, 'space'); - }; - // 更新面包屑 useEffect(() => { - updateBreadcrumbs(); - }, [folderPath]); + updateBreadcrumbs({ + knowledgeBaseFolderPath: folderPath, + documentFolderPath: [], + }); + }, [folderPath, updateBreadcrumbs]); + + // 处理从详情页返回的导航 + useEffect(() => { + const state = location.state as { + navigateToFolder?: string; + folderPath?: Array<{ id: string; name: string }>; + resetToRoot?: boolean; + } | null; + + // 避免重复处理相同的状态 + if (state && state !== processedStateRef.current) { + processedStateRef.current = state; + + if (state.resetToRoot) { + // 重置到根目录 + setFolderPath([]); + setQuery((prev) => ({ + ...prev, + parent_id: undefined, + })); + } else if (state?.navigateToFolder && state?.folderPath) { + // 恢复文件夹路径和查询状态 + setFolderPath(state.folderPath); + setQuery((prev) => ({ + ...prev, + parent_id: state.navigateToFolder, + })); + } + + // 不清除 state,避免干扰后续导航 + // 使用 processedStateRef 来避免重复处理相同的 state + } + }, [location.state, navigate]); useEffect(() => { fetchModelTypes(); @@ -465,7 +468,7 @@ const KnowledgeBaseManagement: FC = () => { if (modelTypes.length) { fetchData(1, false); } - }, [modelTypes, query]) + }, [modelTypes, query.parent_id, query.keywords, query.orderby, query.desc]) return ( <> diff --git a/web/src/views/KnowledgeBase/types.ts b/web/src/views/KnowledgeBase/types.ts index 04c66ac0..f4a6ed56 100644 --- a/web/src/views/KnowledgeBase/types.ts +++ b/web/src/views/KnowledgeBase/types.ts @@ -146,11 +146,19 @@ export interface CreateFolderModalRefProps{ refreshTable?: () => void; } -//他建图片数据集 -export interface CreateImageModalRef{ - handleOpen: (kb_id:string,parent_id:string) => void; +//创建图片数据集 / 创建自定义文本数据集 +export interface CreateSetModalRef{ + handleOpen: (kb_id:string, parent_id:string) => void; } -export interface CreateImageMoealRefProps{ +export interface CreateSetMoealRefProps{ + refreshTable?: () => void; +} + +// 创建内容 +export interface CreateContentModalRef { + handleOpen: (kb_id: string, parent_id: string) => void; +} +export interface CreateContentModalRefProps { refreshTable?: () => void; }