diff --git a/moon/apps/web/components/DiffView/TreeView.tsx b/moon/apps/web/components/DiffView/TreeView.tsx
new file mode 100644
index 000000000..6e7d4ccdf
--- /dev/null
+++ b/moon/apps/web/components/DiffView/TreeView.tsx
@@ -0,0 +1,358 @@
+import * as React from 'react';
+import { useCallback, useEffect, useState } from 'react';
+import ArticleIcon from '@mui/icons-material/Article';
+import FolderRounded from '@mui/icons-material/FolderRounded';
+import FolderOpenIcon from '@mui/icons-material/FolderOpen';
+import { Box, CircularProgress } from '@mui/material';
+import {
+ TreeItemDragAndDropOverlay,
+ TreeItemIcon,
+ TreeItemProvider,
+ useTreeItem,
+ useTreeItemModel,
+ UseTreeItemParameters,
+} from '@mui/x-tree-view';
+import { RichTreeView } from '@mui/x-tree-view/RichTreeView';
+import {
+ TreeItemContent,
+ TreeItemGroupTransition,
+ TreeItemIconContainer,
+ TreeItemLabel,
+ TreeItemRoot,
+} from '@mui/x-tree-view/TreeItem';
+import { usePathname } from 'next/navigation';
+import { useRouter } from 'next/router';
+import { styled, alpha } from '@mui/material/styles';
+import { useGetTree } from '@/hooks/useGetTree';
+import { v4 as uuidv4 } from 'uuid';
+
+interface MuiTreeNode {
+ id?: string;
+ label?: string;
+ path?: string;
+ content_type?: FileType;
+ isLeaf?: boolean;
+ children?: MuiTreeNode[];
+}
+
+type FileType = 'file' | 'directory';
+
+interface ExtendedTreeItemProps {
+ content_type?: FileType;
+ id: string;
+ label: string;
+}
+
+interface CustomLabelProps {
+ children: React.ReactNode;
+ icon?: React.ElementType;
+ expandable?: boolean;
+}
+
+function CustomLabel({ icon: Icon, children, ...other }: CustomLabelProps) {
+ return (
+
+ {Icon && (
+
+ )}
+ {children}
+
+ );
+}
+
+const getIconFromFileType = (fileType: FileType, isExpanded:Boolean) => {
+ switch (fileType) {
+ case 'file':
+ return ArticleIcon;
+ case 'directory':
+ return isExpanded ? FolderOpenIcon :FolderRounded;
+ default:
+ return ArticleIcon;
+ }
+};
+
+interface CustomTreeItemProps
+ extends Omit
,
+ Omit, 'onFocus'> {
+ }
+
+const CustomTreeItem = React.forwardRef(function CustomTreeItem(
+ { ...props }: CustomTreeItemProps,
+ ref: React.Ref,
+) {
+ const { id, itemId, label, disabled, children, ...other } = props;
+ const {
+ getContextProviderProps,
+ getRootProps,
+ getContentProps,
+ getIconContainerProps,
+ getLabelProps,
+ getGroupTransitionProps,
+ getDragAndDropOverlayProps,
+ status,
+ } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref });
+
+ const item = useTreeItemModel(itemId)!;
+ let icon;
+
+ if (status.expandable) {
+ icon = getIconFromFileType(item.content_type || 'file', status.expanded);
+ } else if (item.content_type) {
+ icon = getIconFromFileType(item.content_type, false);
+ }
+
+ const StyledGroupTransition = styled(TreeItemGroupTransition)(({ theme }) => ({
+ marginLeft: 15,
+ borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`,
+ }));
+
+ return (
+
+
+ {item.label === '加载中...' ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ {/* label */}
+
+
+
+ )}
+ {children && }
+
+
+ );
+});
+
+const RepoTree = ({ directory }: { directory: any[] }) => {
+ const router = useRouter();
+ const scope = router.query.org as string;
+ const pathname = usePathname();
+ const basePath = pathname?.replace(
+ new RegExp(`\\/${scope}\\/code\\/(tree|blob)`),
+ ''
+ );
+
+ const [treeData, setTreeData] = useState([]);
+
+ const [expandedNodes, setExpandedNodes] = useState([]);
+ const [selectedNode, setSelectedNode] = useState(null);
+
+ const [loadPath, setLoadPath] = useState(null);
+ const [targetNodeId, setTargetNodeId] = useState(null);
+
+ const { data: response, isLoading, error } = useGetTree({ path: loadPath??undefined});
+
+ const convertToTreeData = useCallback((parentBasePath: string, directory: any[]) => {
+ if (!Array.isArray(directory) || directory.length === 0) {
+ // console.warn('convertToTreeData: 接收到非数组或空数据', directory);
+ return [];
+ }
+
+ return sortProjectsByType(directory).map((item) => {
+
+ const currentPath = `${parentBasePath}/${item.name}`.replace('//', '/') || '/';
+ // console.log('生成节点路径:', item.name, '=>', currentPath);
+
+ return {
+ id: uuidv4(),
+ label: '我是文件夹',
+ path: currentPath,
+ isLeaf: item.content_type !== 'directory',
+ content_type: item.content_type,
+ children: item.content_type === 'directory' ? [
+ {
+ id: uuidv4(),
+ label: '加载中...',
+ isLeaf: true,
+ },
+ ] : undefined,
+ };
+ });
+ }, []);
+
+ useEffect(() => {
+
+ const rootPath = basePath || '/'; // 使用基路径作为根路径
+
+ setTreeData(convertToTreeData(rootPath, directory));
+ }, [directory, convertToTreeData,basePath]);
+
+ const sortProjectsByType = (projects: any[]) => {
+ if (!Array.isArray(projects) || projects.length === 0) {
+ // console.warn('sortProjectsByType: 接收到非数组或空数据', projects);
+ return [];
+ }
+
+ return projects.sort((a, b) => {
+ if (a.content_type === 'directory' && b.content_type === 'file') {
+ return -1;
+ } else if (a.content_type === 'file' && b.content_type === 'directory') {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+ };
+
+ const updateTreeData = useCallback(
+ (currentTree: MuiTreeNode[], nodeId: string, newChildren: MuiTreeNode[]): any => {
+ if (!Array.isArray(currentTree) || currentTree.length === 0) {
+ // console.warn('updateTreeData: 接收到非数组或空的当前树数据', currentTree);
+ return [];
+ }
+
+ return currentTree.map((node) => {
+ if (node.id === nodeId) {
+ return {
+ ...node,
+ children: newChildren,
+ isLeaf: newChildren.length === 0,
+ };
+ }
+ if (node.children && node.children.length > 0) {
+ return {
+ ...node,
+ children: updateTreeData(node.children, nodeId, newChildren),
+ };
+ }
+ return node;
+ });
+ },
+ [],
+ );
+
+ const findNode = useCallback(
+ (data: MuiTreeNode[], nodeId: string): MuiTreeNode | null => {
+ if (!Array.isArray(data) || data.length === 0) {
+ return null;
+ }
+
+ for (const node of data) {
+
+ if (node.id === nodeId) return node;
+
+ if (node.children) {
+ const found = findNode(node.children, nodeId);
+
+ if (found) return found;
+ }
+ }
+ return null;
+ },
+ [],
+ );
+
+ const handleNodeToggle = useCallback(
+ (_event: React.SyntheticEvent | null, nodeIds: string[]) => {
+ const newlyExpandedId = nodeIds.find((id) => !expandedNodes.includes(id));
+
+ if (newlyExpandedId) {
+ const targetNode = findNode(treeData, newlyExpandedId);
+
+ const reqPath = targetNode?.path?.startsWith('/')
+ ? targetNode.path
+ : `/${targetNode?.path}`;
+
+ if (
+ targetNode &&
+ targetNode.content_type === 'directory' &&
+ targetNode.children?.[0]?.label === '加载中...' // 中文判断
+ ) {
+ setLoadPath(reqPath);
+ setTargetNodeId(newlyExpandedId);
+ // setLoadingError(null);
+ }
+ }
+
+ setExpandedNodes(nodeIds);
+ },
+ [expandedNodes, treeData, setLoadPath, setTargetNodeId, findNode],
+ );
+
+ useEffect(() => {
+ if (!loadPath) return;
+
+ if (response && !isLoading) {
+ if (
+ !response.req_result ||
+ !Array.isArray(response.data) ||
+ response.data.length === 0
+ ) {
+ // console.error('API数据格式错误:', response);
+ // setLoadingError('数据格式错误或为空');
+ return;
+ }
+
+ const items = response.data;
+ const newChildren = convertToTreeData(loadPath, items);
+ const newTree = updateTreeData(treeData, targetNodeId!, newChildren);
+
+ setTreeData(newTree);
+ setLoadPath(null);
+ setTargetNodeId(null);
+ }
+
+ // if (error) {
+ // console.error('Error fetching data:', error);
+ // // setLoadingError(error.message || '加载失败');
+ // }
+ }, [response, isLoading, error, loadPath, targetNodeId, treeData, convertToTreeData, updateTreeData]);
+
+
+ const handleNodeSelect = useCallback(
+ (_event: React.SyntheticEvent | null, nodeId: string | null) => {
+ if (!nodeId) return;
+
+ setSelectedNode(nodeId);
+ const node = findNode(treeData, nodeId);
+
+ if (!node) return;
+
+ },
+ [treeData, findNode],
+ );
+
+ return (
+ <>
+ (
+
+ )
+ }}
+ >
+
+ >
+ );
+};
+
+export default RepoTree;
\ No newline at end of file
diff --git a/moon/apps/web/pages/[org]/code/blob/[...path].tsx b/moon/apps/web/pages/[org]/code/blob/[...path].tsx
index 8ab2d1731..7b3765125 100644
--- a/moon/apps/web/pages/[org]/code/blob/[...path].tsx
+++ b/moon/apps/web/pages/[org]/code/blob/[...path].tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
import { Flex, Layout } from 'antd'
import BreadCrumb from '@/components/CodeView/TreeView/BreadCrumb'
import CodeContent from '@/components/CodeView/BlobView/CodeContent'
@@ -8,6 +8,9 @@ import { useGetBlob } from '@/hooks/useGetBlob'
import { useRouter } from 'next/router'
import { CommentSection } from '@/components/CodeView/BlobView/CommentSection'
import CommitHistory, { CommitInfo } from '@/components/CodeView/CommitHistory'
+import RepoTree from '@/components/CodeView/TreeView/RepoTree'
+import { CommonResultVecTreeCommitItem } from '@gitmono/types/generated'
+import { useGetTreeCommitInfo } from '@/hooks/useGetTreeCommitInfo'
const codeStyle = {
borderRadius: 8,
@@ -17,6 +20,14 @@ const codeStyle = {
margin: '0 8px'
}
+const treeStyle = {
+ borderRadius: 8,
+ overflow: 'hidden',
+ width: 'calc(20% - 8px)',
+ maxWidth: 'calc(20% - 8px)',
+ background: '#fff'
+}
+
interface Comment {
id: string
content: string
@@ -77,6 +88,37 @@ function BlobPage() {
hash: '5fe4235',
date: '3 months ago'
}
+
+ const newPath = new_path?.split("/").slice(0, -1).join("/")
+ const { data: TreeCommitInfo } = useGetTreeCommitInfo(newPath)
+
+ type DirectoryType = NonNullable
+ const directory: DirectoryType = useMemo(() => TreeCommitInfo?.data ?? [], [TreeCommitInfo])
+ const [newDirectory, setNewDirectory] = useState([])
+
+ // eslint-disable-next-line no-console
+ console.log(directory, 'directory==directory')
+
+ useEffect(()=>{
+ const handleDirectory = () => {
+ const filteredItems = directory.filter((item) => {
+ return item.content_type !== 'directory';
+ });
+
+ // eslint-disable-next-line no-console
+ console.log(filteredItems, 'filteredItems==');
+ setNewDirectory(filteredItems); // 直接设置过滤后的数组,而非嵌套数组
+ };
+
+ handleDirectory()
+
+ },[directory, setNewDirectory])
+
+
+
+
+
+
const handleAddComment = (__content: string, __lineNumber?: number) => {
//wait for complete
@@ -93,17 +135,26 @@ function BlobPage() {
-
-
+ {/* tree */}
+
+
+
-
-
-
-
-
- {/* @ts-ignore */}
-
+
+
+
+
+
+
+
+
+
+ {/* @ts-ignore */}
+
+
+
+
diff --git a/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx b/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx
index ced51ea23..eb3da8efc 100644
--- a/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx
+++ b/moon/apps/web/pages/[org]/code/tree/[...path]/index.tsx
@@ -89,7 +89,7 @@ function TreeDetailPage() {
{/* tree */}