feat: Add base project structure with API and web components

This commit is contained in:
Ke Sun
2025-12-02 20:28:01 +08:00
parent f3de6d6cc9
commit c1adc62ec6
817 changed files with 111226 additions and 106 deletions

View File

@@ -0,0 +1,365 @@
import { useMemo, useEffect, useState } from 'react';
import type { FC } from 'react';
import type { CSSProperties, Key, ReactNode } from 'react';
import { Tree } from 'antd';
import type { DataNode, TreeProps } from 'antd/es/tree';
import folderIcon from '@/assets/images/knowledgeBase/folder.png';
import textIcon from '@/assets/images/knowledgeBase/text.png';
import imageIcon from '@/assets/images/knowledgeBase/image.png';
import datasetsIcon from '@/assets/images/knowledgeBase/datasets.png';
import switcherIcon from '@/assets/images/knowledgeBase/switcher.png';
import { getFolderList } from '../service';
const { DirectoryTree } = Tree;
const TEXT_EXTENSIONS = new Set([
'txt',
'md',
'rtf',
'doc',
'docx',
'pdf',
'csv',
'json',
'xml',
'html',
'htm',
'log',
]);
const IMAGE_EXTENSIONS = new Set([
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'webp',
'svg',
'tiff',
'ico',
]);
export interface TreeNodeData {
key: Key;
title: ReactNode;
icon?: string;
switcherIcon?: string;
type?: string;
isLeaf?: boolean;
children?: TreeNodeData[];
}
interface FolderTreeProps {
knowledgeBaseId: string;
onSelect?: TreeProps['onSelect'];
onExpand?: TreeProps['onExpand'];
multiple?: boolean;
className?: string;
style?: CSSProperties;
refreshKey?: number;
onRootLoad?: (nodes: TreeNodeData[] | null) => void;
}
const renderIcon = (icon?: string) => {
if (!icon) return undefined;
return <img src={icon} alt="icon" style={{ width: 16, height: 16 }} />;
};
const transformTreeData = (nodes: TreeNodeData[]): DataNode[] =>
nodes.map((node) => {
const children = node.children && node.children.length > 0 ? transformTreeData(node.children) : undefined;
return {
key: node.key,
title: node.title ?? '',
icon: renderIcon(node.icon),
switcherIcon: renderIcon(node.switcherIcon),
isLeaf: node.isLeaf,
children,
};
});
const buildMockTreeData = (): TreeNodeData[] => ([
{
title: '数据集文件夹',
key: '0',
icon: folderIcon,
switcherIcon: switcherIcon,
type: 'folder',
children: [
{
title: '文本数据集',
key: '0-0',
icon: textIcon,
switcherIcon: switcherIcon,
type: 'text',
children: [
{
title: '子文件夹1',
key: '0-0-0',
icon: folderIcon,
switcherIcon: switcherIcon,
type: 'folder',
children: [
{
title: '文档1.txt',
key: '0-0-0-0',
icon: textIcon,
type: 'text',
},
{
title: '文档2.txt',
key: '0-0-0-1',
icon: textIcon,
type: 'text',
},
],
},
{
title: '子文件夹2',
key: '0-0-1',
icon: folderIcon,
switcherIcon: switcherIcon,
type: 'folder',
children: [
{
title: '嵌套文件夹',
key: '0-0-1-0',
icon: folderIcon,
switcherIcon: switcherIcon,
type: 'folder',
children: [
{
title: '深度文档.txt',
key: '0-0-1-0-0',
icon: textIcon,
type: 'text',
},
],
},
],
},
],
},
{
title: '图片数据集',
key: '0-1',
icon: imageIcon,
switcherIcon: switcherIcon,
type: 'image',
children: [
{
title: '图片1.jpg',
key: '0-1-0',
icon: imageIcon,
type: 'image',
},
{
title: '图片2.png',
key: '0-1-1',
icon: imageIcon,
type: 'image',
},
],
},
{
title: '通用数据集',
key: '0-2',
icon: datasetsIcon,
type: 'dataset',
},
],
},
]);
const normalizeExt = (ext?: string): string => {
if (typeof ext !== 'string') return '';
return ext.trim().replace(/^\./, '').toLowerCase();
};
const isFolderLike = (node: any): boolean => {
const ext = normalizeExt(node?.file_ext);
if (ext) {
return ext === 'folder';
}
const type = typeof node?.type === 'string' ? node.type.toLowerCase() : '';
if (type === 'folder' || type === 'directory') return true;
if (typeof node?.is_directory === 'boolean') return node.is_directory;
if (typeof node?.is_dir === 'boolean') return node.is_dir;
if (node?.folder_name || node?.children) return true;
return false;
};
const getNodeTitle = (node: any): string => (
node?.folder_name
?? node?.file_name
?? node?.name
?? node?.title
?? '未命名节点'
);
const getNodeIcon = (node: any, isFolder: boolean): string => {
if (isFolder) return folderIcon;
const type = typeof node?.type === 'string' ? node.type.toLowerCase() : '';
if (type === 'image') return imageIcon;
if (type === 'text') return textIcon;
const ext = normalizeExt(node?.file_ext);
if (IMAGE_EXTENSIONS.has(ext)) return imageIcon;
if (TEXT_EXTENSIONS.has(ext)) return textIcon;
return datasetsIcon;
};
const extractItems = (resp: any): any[] => {
if (!resp) return [];
if (Array.isArray(resp)) return resp;
if (Array.isArray(resp?.items)) return resp.items;
if (Array.isArray(resp?.list)) return resp.list;
if (Array.isArray(resp?.data?.items)) return resp.data.items;
return [];
};
// 只加载当前层级的节点,不递归加载子节点
const buildTreeNodes = async (
kbId: string,
parentId: string,
): Promise<TreeNodeData[]> => {
const currentParent = String(parentId ?? '');
if (!currentParent) return [];
// 只请求一次当前层级的数据,不分页
const response = await getFolderList({
kb_id: kbId,
parent_id: currentParent,
page: 1,
pagesize: 1000
} as any);
const rawItems = extractItems(response);
const nodes: TreeNodeData[] = [];
for (let index = 0; index < rawItems.length; index += 1) {
const raw = rawItems[index];
const keySource = raw?.id ?? raw?.file_id ?? raw?.key ?? raw?.folder_id ?? `${currentParent}-${index}`;
const nodeKey = String(keySource);
const isFolder = isFolderLike(raw);
// 只显示文件夹
if (!isFolder) {
continue;
}
// 文件夹节点初始不加载子节点isLeaf设为false表示可能有子节点
nodes.push({
key: nodeKey,
title: getNodeTitle(raw),
icon: getNodeIcon(raw, isFolder),
switcherIcon: isFolder ? switcherIcon : undefined,
type: isFolder ? 'folder' : (typeof raw?.type === 'string' ? raw.type : normalizeExt(raw?.file_ext) || 'file'),
isLeaf: false, // 文件夹节点初始设为false表示可能有子节点需要展开时加载
children: undefined, // 初始不加载子节点
});
}
return nodes;
};
const FolderTree: FC<FolderTreeProps> = ({
knowledgeBaseId,
onSelect,
onExpand,
multiple,
className,
style,
refreshKey = 0,
onRootLoad,
}) => {
const [treeData, setTreeData] = useState<TreeNodeData[]>([]);
// 更新树节点数据的辅助函数
const updateTreeData = (nodes: TreeNodeData[], key: Key, children: TreeNodeData[]): TreeNodeData[] => {
return nodes.map((node) => {
if (node.key === key) {
return {
...node,
children: children.length > 0 ? children : undefined,
isLeaf: children.length === 0,
};
}
if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
return node;
});
};
// 加载根节点
useEffect(() => {
let cancelled = false;
const load = async () => {
if (!knowledgeBaseId) {
setTreeData([]);
return;
}
try {
const nodes = await buildTreeNodes(knowledgeBaseId, knowledgeBaseId);
if (!cancelled) {
setTreeData(nodes);
if (onRootLoad) {
onRootLoad(nodes.length > 0 ? nodes : null);
}
}
} catch (e) {
console.error('加载文件夹树失败:', e);
if (!cancelled) {
const fallback = buildMockTreeData();
setTreeData(fallback);
if (onRootLoad) {
onRootLoad(fallback.length > 0 ? fallback : null);
}
}
}
};
load();
return () => {
cancelled = true;
};
}, [knowledgeBaseId, refreshKey]);
// 懒加载子节点 - 只在展开时加载
const onLoadData = async (node: any) => {
const { key } = node;
// 如果已经加载过子节点,不再重复加载
if (node.children !== undefined) {
return Promise.resolve();
}
try {
// 使用节点的 key 作为 parent_id 加载子文件夹
const children = await buildTreeNodes(knowledgeBaseId, String(key));
setTreeData((prevData) => updateTreeData(prevData, key, children));
} catch (e) {
console.error('加载子节点失败:', e);
// 加载失败时,将该节点标记为叶子节点(没有子节点)
setTreeData((prevData) => updateTreeData(prevData, key, []));
}
};
const treeNodes = useMemo(() => transformTreeData(treeData), [treeData]);
return (
<DirectoryTree
multiple={multiple}
className={className}
style={style}
onSelect={onSelect}
onExpand={onExpand}
loadData={onLoadData}
treeData={treeNodes}
/>
);
};
export default FolderTree;