Merge #4 into develop_web from web

合并 feature/20251219_yjp 分支到 web 分支

* web: (11 commits)
  update web
  update web
  update web
  update web
  update web
  update web
  Sync frontend project from dev-yjp branch
  feat(web): Update auto-imports with new React hooks and components
  feat:create cusotm text dataset
  feat(web): update auto-imports with new React hooks and components
  合并 feature/20251219_yjp 分支到 web 分支

Signed-off-by: zhaoying <zhaoying@redbearai.com>
Merged-by: zhaoying <zhaoying@redbearai.com>

CR-link: https://codeup.aliyun.com/redbearai/python/redbear-mem-open/change/4
This commit is contained in:
赵莹
2025-12-18 10:23:01 +08:00
19 changed files with 994 additions and 319 deletions

View File

@@ -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

View File

@@ -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')

View File

@@ -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;
};
// 文档分块预览

View File

@@ -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<SettingModalRef>(null)
const userInfoModalRef = useRef<UserInfoModalRef>(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 = () => {

View File

@@ -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,
};
};

View File

@@ -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]);

View File

@@ -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',

View File

@@ -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: '图片',

View File

@@ -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') || '[]'

View File

@@ -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<number>(stepIndexMap[initialStepKey]);
const tableRef = useRef<TableRef>(null);
const [data, setData] = useState<KnowledgeBaseDocumentData[]>([]);
const [chunkData, setChunkData] = useState<any[]>([]);
const [total, setTotal] = useState<number>(0);
const [rechunkFileIds, setRechunkFileIds] = useState<string[]>(initialFileIds);
const [curSelectedFileId, setCurSelectedFileId] = useState<number>(-1);
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
const [pollingLoading, setPollingLoading] = useState<boolean>(false);
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [delimiter, setDelimiter] = useState<string | undefined>(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 ? (
<Table
ref={tableRef}
apiUrl={`/documents/${knowledgeBaseId}/${parentId}/documents`}
apiParams={{
apiUrl={`/documents/${knowledgeBaseId}/documents`}
apiParams={{
document_ids: rechunkFileIds.join(','),
}}
columns={columns}

View File

@@ -4,11 +4,12 @@
* @Author: yujiangping
* @Date: 2025-11-15 16:13:47
* @LastEditors: yujiangping
* @LastEditTime: 2025-11-29 19:46:46
* @LastEditTime: 2025-12-12 20:02:05
*/
import { useEffect, useState, useRef, type FC } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useBreadcrumbManager, type BreadcrumbPath } from '@/hooks/useBreadcrumbManager';
import { Button, Spin, message, Switch } from 'antd';
import { getDocumentDetail, getDocumentChunkList, downloadFile, updateDocument, updateDocumentChunk, createDocumentChunk } from '@/api/knowledgeBase';
import type { KnowledgeBaseDocumentData, RecallTestData } from '@/views/KnowledgeBase/types';
@@ -25,7 +26,18 @@ const DocumentDetails: FC = () => {
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<KnowledgeBaseDocumentData | null>(null);
const [chunkList, setChunkList] = useState<RecallTestData[]>([]);
@@ -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`);
}
};

View File

@@ -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<string | undefined>(undefined);
const recallTestDrawerRef = useRef<RecallTestDrawerRef>(null);
const createFolderModalRef = useRef<CreateFolderModalRef>(null);
const createImageDataset = useRef<CreateImageModalRef>(null)
const createImageDataset = useRef<CreateSetModalRef>(null)
const createContentModalRef = useRef<CreateSetModalRef>(null);
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseListItem | null>(null);
const [folder, setFolder] = useState<FolderFormData | null>({
kb_id:knowledgeBaseId ?? '',
@@ -56,47 +60,47 @@ const Private: FC = () => {
});
const [query, setQuery] = useState<Record<string, unknown>>({
orderby: 'created_at',
desc: true,
desc: true
});
const modalRef = useRef<CreateModalRef>(null)
const shareModalRef = useRef<ShareModalRef>(null);
const datasetModalRef = useRef<CreateDatasetModalRef>(null);
const [folderTreeRefreshKey, setFolderTreeRefreshKey] = useState(0);
const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu();
const [folderPath, setFolderPath] = useState<Array<{ id: string; name: string }>>([]);
const [autoExpandPath, setAutoExpandPath] = useState<Array<{ id: string; name: string }>>([]);
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<BreadcrumbItem[]>([]);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
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<BreadcrumbItem[]>([]);
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: <img src={blankIcon} alt="Custome Text" style={{ width: 16, height: 16 }} />,
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}
/>
</div>
)}
@@ -601,6 +684,10 @@ const Private: FC = () => {
ref={createFolderModalRef}
refreshTable={refreshDirectoryTree}
/>
<CreateContentModal
ref={createContentModalRef}
refreshTable={handleRefreshTable}
/>
<CreateModal
ref={modalRef}
refreshTable={handleRefreshTable}

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useRef, type FC } from 'react';
import { useParams } from 'react-router-dom';
import { useParams, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import type { KnowledgeBaseListItem, RecallTestDrawerRef } from '@/views/KnowledgeBase/types';
import RecallTest from '../components/RecallTest';
@@ -15,17 +15,22 @@ import kbModelIcon from '@/assets/images/knowledgeBase/kb-model.png';
import kbHistoryIcon from '@/assets/images/knowledgeBase/kb-history.png';
import { getKnowledgeBaseDetail } from '@/api/knowledgeBase';
import { formatDateTime } from '@/utils/format';
import { useMenu } from '@/store/menu';
import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager';
const Share: FC = () => {
const { t } = useTranslation();
const params = useParams<{ knowledgeBaseId: string }>();
const location = useLocation();
const knowledgeBaseId = params.knowledgeBaseId;
const [loading, setLoading] = useState(false);
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBaseListItem | null>(null);
const recallTestRef = useRef<RecallTestDrawerRef>(null);
const [infoItems, setInfoItems] = useState<InfoItem[]>([]);
const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu();
const [knowledgeBaseFolderPath, setKnowledgeBaseFolderPath] = useState<BreadcrumbItem[]>([]);
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');

View File

@@ -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<CreateSetModalRef, CreateSetMoealRefProps>(
({ refreshTable }, ref) => {
const { t } = useTranslation();
const navigate = useNavigate();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<ContentFormData>();
const [loading, setLoading] = useState(false);
const [kbId, setKbId] = useState<string>('');
const [parentId, setParentId] = useState<string>('');
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 (
<RbModal
title={t('knowledgeBase.createContent')}
open={visible}
onCancel={handleClose}
okText={t('common.create')}
onOk={handleSave}
confirmLoading={loading}
width={600}
>
<Form form={form} layout="vertical">
<Form.Item
name="title"
label={t('knowledgeBase.title')}
rules={[{ required: true, message: t('knowledgeBase.pleaseEnterTitle') }]}
>
<Input placeholder={t('knowledgeBase.pleaseEnterTitle')} />
</Form.Item>
<Form.Item
name="content"
label={t('knowledgeBase.content')}
rules={[{ required: true, message: t('knowledgeBase.pleaseEnterContent') }]}
>
<Input.TextArea
placeholder={t('knowledgeBase.pleaseEnterContent')}
rows={8}
showCount
maxLength={5000}
/>
</Form.Item>
</Form>
</RbModal>
);
}
);
export default CreateContentModal;

View File

@@ -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<CreateContentModalRef>(null);
const handleOpenModal = () => {
// 打开弹窗传入知识库ID和父级ID
createContentModalRef.current?.handleOpen('kb_123', 'parent_456');
};
const handleRefreshTable = () => {
console.log('刷新表格数据');
// 这里可以添加刷新表格的逻辑
};
return (
<div>
<Button type="primary" onClick={handleOpenModal}>
</Button>
<CreateContentModal
ref={createContentModalRef}
refreshTable={handleRefreshTable}
/>
</div>
);
};
export default CreateContentModalExample;

View File

@@ -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<CreateImageModalRef, CreateImageMoealRefProps>(
const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>(
({ refreshTable }, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);

View File

@@ -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<FolderTreeProps> = ({
onRootLoad,
onFolderPathChange,
selectedKeys,
autoExpandPath,
}) => {
const [treeData, setTreeData] = useState<TreeNodeData[]>([]);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [autoExpandInProgress, setAutoExpandInProgress] = useState(false);
// 更新树节点数据的辅助函数
const updateTreeData = (nodes: TreeNodeData[], key: Key, children: TreeNodeData[]): TreeNodeData[] => {
@@ -370,6 +375,109 @@ const FolderTree: FC<FolderTreeProps> = ({
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<FolderTreeProps> = ({
return (
<DirectoryTree
key={refreshKey} // 添加key确保refreshKey变化时重新渲染整个组件
multiple={multiple}
className={className}
style={style}
onSelect={handleSelect}
onExpand={onExpand}
onExpand={handleExpand}
expandedKeys={expandedKeys}
loadData={onLoadData}
treeData={treeNodes}
selectedKeys={selectedKeys}

View File

@@ -1,8 +1,8 @@
import { useEffect, useState, useRef, useMemo, type FC } from 'react';
import { useEffect, useState, useRef, useMemo, useCallback, type FC } from 'react';
import { Row, Col, Button, Dropdown, Modal, message, Tooltip } from 'antd'
import type { MenuProps } from 'antd';
import { EllipsisOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
@@ -18,7 +18,8 @@ import Empty from '@/components/Empty'
import { getKnowledgeBaseList, getModelList, getModelTypeList, deleteKnowledgeBase, getKnowledgeBaseTypeList } from '@/api/knowledgeBase'
const { confirm } = Modal;
import InfiniteScroll from 'react-infinite-scroll-component';
import { useMenu } from '@/store/menu';
import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager';
type ModelMenuInfo = {
menu: NonNullable<MenuProps['items']>;
@@ -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<KnowledgeBaseListItem[]>([])
const [page, setPage] = useState(1)
@@ -42,10 +44,29 @@ const KnowledgeBaseManagement: FC = () => {
const modelListCache = useRef<Record<string, string>>({});
const modalRef = useRef<CreateModalRef>(null)
const [messageApi, contextHolder] = message.useMessage();
const processedStateRef = useRef<any>(null);
// 使用 menu store 管理面包屑
const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu();
const [folderPath, setFolderPath] = useState<Array<{ id: string; name: string }>>([]);
// 使用面包屑管理 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<BreadcrumbItem[]>([]);
// 生成下拉菜单项(根据当前 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 (
<>

View File

@@ -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;
}