From 39736fb54f0ba8352c1ebbed20df5415ade3772d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 5 Sep 2026 18:46:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20UI=20=E6=A0=91=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E6=93=8D=E4=BD=9C=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一节点操作列宽度并对齐可见性与删除按钮 为树内容增加横向滚动和动态宽度测量 组件数量改用实心橙色拼图图标徽标 --- .../view/ui-editor/components/UiTreePanel.tsx | 184 ++++++++++++++---- 1 file changed, 149 insertions(+), 35 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/UiTreePanel.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/UiTreePanel.tsx index ed2169fd3..ae521ce83 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/UiTreePanel.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/UiTreePanel.tsx @@ -1,7 +1,10 @@ -import { ChevronDown, Eye, EyeOff, Trash2 } from 'lucide-react'; +import { ChevronDown, Eye, EyeOff, Puzzle, Trash2 } from 'lucide-react'; import { + forwardRef, + type HTMLAttributes, type MouseEvent as ReactMouseEvent, useEffect, + useMemo, useRef, useState, } from 'react'; @@ -9,6 +12,7 @@ import { adjustMoveIndex, type MoveHandler, type NodeRendererProps, + type RowRendererProps, Tree, type TreeApi, } from 'react-arborist'; @@ -44,6 +48,38 @@ type UiTreePanelProps = { className?: string; }; +const TreeListOuter = forwardRef< + HTMLDivElement, + HTMLAttributes +>(function TreeListOuter({ style, ...props }, ref) { + return ( +
+ ); +}); + +function TreeRowContainer({ + node, + innerRef, + attrs, + children, +}: RowRendererProps) { + return ( +
event.stopPropagation()} + onClick={node.handleClick} + > + {children} +
+ ); +} + function TreeRow({ node, style, @@ -70,8 +106,8 @@ function TreeRow({ return (
{ @@ -102,38 +138,53 @@ function TreeRow({ /> ) : null} - {nodeLabel} - - {data.components.length} + + {nodeLabel} - - + + + + +
); } @@ -155,7 +206,9 @@ export function UiTreePanel({ }: UiTreePanelProps) { const treeApiRef = useRef | undefined>(undefined); const treeContainerRef = useRef(null); + const treeSizerRef = useRef(null); const [treeHeight, setTreeHeight] = useState(0); + const [treeWidth, setTreeWidth] = useState(0); const [contextMenu, setContextMenu] = useState<{ nodeId: NodeId; x: number; @@ -182,6 +235,33 @@ export function UiTreePanel({ return () => resizeObserver.disconnect(); }, []); + const treeRows = useMemo(() => { + if (!root) return []; + const rows: Array<{ node: UiNode; depth: number }> = []; + const visit = (node: UiNode, depth: number) => { + rows.push({ node, depth }); + node.children.forEach((child) => visit(child, depth + 1)); + }; + visit(root, 0); + return rows; + }, [root]); + + useEffect(() => { + const sizer = treeSizerRef.current; + const container = treeContainerRef.current; + if (!sizer || !container) return; + const updateWidth = () => { + setTreeWidth( + Math.max(Math.ceil(sizer.scrollWidth), container.clientWidth), + ); + }; + updateWidth(); + const resizeObserver = new ResizeObserver(updateWidth); + resizeObserver.observe(sizer); + resizeObserver.observe(container); + return () => resizeObserver.disconnect(); + }, [treeRows]); + const handleMove: MoveHandler = ({ dragIds, parentId, @@ -250,16 +330,50 @@ export function UiTreePanel({ className={`relative flex h-full min-h-0 flex-col ${className ?? ''}`} onContextMenu={(event) => event.preventDefault()} > -
+
+ {root && treeHeight > 0 ? ( ref={treeApiRef} data={[root]} - width="100%" + width={treeWidth > 0 ? treeWidth : '100%'} + outerElementType={TreeListOuter} height={treeHeight} rowHeight={34} indent={16} openByDefault + renderRow={TreeRowContainer} selection={selectedNodeId ?? undefined} onSelect={(nodes) => { const selected = nodes[0]?.data;