diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/treeUtils.ts b/apps/ai-game-creator-shell/src/features/ui-editor/treeUtils.ts index 9e044efb8..52d0fa137 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/treeUtils.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/treeUtils.ts @@ -14,3 +14,20 @@ export function collectUiNodeIds(root: UiNode): Set { visitUiNodes(root, (node) => ids.add(node.id)); return ids; } + +export function findUiNode(root: UiNode, nodeId: NodeId): UiNode | null { + if (root.id === nodeId) return root; + for (const child of root.children) { + const found = findUiNode(child, nodeId); + if (found) return found; + } + return null; +} + +export function countUiNodeDescendants(root: UiNode): number { + let count = 0; + for (const child of root.children) { + count += 1 + countUiNodeDescendants(child); + } + return count; +} diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/UiNodeDeleteConfirmModal.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/UiNodeDeleteConfirmModal.tsx new file mode 100644 index 000000000..08e5362d3 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/UiNodeDeleteConfirmModal.tsx @@ -0,0 +1,56 @@ +import { ThemedModal } from '../../../components/modal/ThemedModal'; +import type { NodeId } from '../../../features/ui-editor/types/NodeId'; + +export type UiNodeDeleteRequest = { + nodeId: NodeId; + nodeLabel: string; + descendantCount: number; +}; + +type UiNodeDeleteConfirmModalProps = { + request: UiNodeDeleteRequest | null; + onCancel: () => void; + onConfirm: (nodeId: NodeId) => void; + disabled?: boolean; +}; + +export function UiNodeDeleteConfirmModal({ + request, + onCancel, + onConfirm, + disabled = false, +}: UiNodeDeleteConfirmModalProps) { + return ( + +

确认删除节点?

+

+ 将删除「{request?.nodeLabel ?? ''}」及其 {request?.descendantCount ?? 0}{' '} + 个后代节点,是否继续? +

+
+ + +
+
+ ); +} 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 5974e3ef7..ed2169fd3 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,4 +1,4 @@ -import { ChevronDown, Eye, EyeOff } from 'lucide-react'; +import { ChevronDown, Eye, EyeOff, Trash2 } from 'lucide-react'; import { type MouseEvent as ReactMouseEvent, useEffect, @@ -13,12 +13,20 @@ import { type TreeApi, } from 'react-arborist'; +import { + countUiNodeDescendants, + findUiNode, +} from '../../../features/ui-editor/treeUtils'; import type { Node as UiNode } from '../../../features/ui-editor/types/Node'; import type { NodeId } from '../../../features/ui-editor/types/NodeId'; import type { UIDesignImageId } from '../../../features/ui-editor/types/UIDesignImageId'; import type { UiNodeMoveRequest } from '../../../features/ui-editor/types/UiNodeMoveRequest'; import type { UiEditorNodeFocusRequest } from '../model'; import { UiNodeContextMenu } from './UiNodeContextMenu'; +import { + UiNodeDeleteConfirmModal, + type UiNodeDeleteRequest, +} from './UiNodeDeleteConfirmModal'; type UiTreePanelProps = { root: UiNode | null; @@ -44,11 +52,17 @@ function TreeRow({ isNodeVisible, onToggleNodeVisibility, onOpenContextMenu, + onRequestDelete, + canDelete, + deleteDisabled, }: NodeRendererProps & { onSelectNode: (id: NodeId) => void; isNodeVisible: (nodeId: NodeId) => boolean; onToggleNodeVisibility: (id: NodeId) => void; onOpenContextMenu: (event: ReactMouseEvent, id: NodeId) => void; + onRequestDelete: (id: NodeId) => void; + canDelete: boolean; + deleteDisabled: boolean; }) { const data = node.data; const nodeLabel = data.metadata.name || '未命名节点'; @@ -105,6 +119,21 @@ function TreeRow({ > {isVisible ? : } + ); } @@ -132,6 +161,9 @@ export function UiTreePanel({ x: number; y: number; } | null>(null); + const [pendingDelete, setPendingDelete] = useState< + (UiNodeDeleteRequest & { treeId: UIDesignImageId }) | null + >(null); useEffect(() => { if (!focusRequest) return; @@ -184,6 +216,35 @@ export function UiTreePanel({ const pageRootIds = new Set(root?.children.map((child) => child.id) ?? []); const contextTreeId = contextMenu ? treeIdForNode(contextMenu.nodeId) : null; + const requestDelete = (nodeId: NodeId) => { + if (!root || isLocked || nodeId === root.id || pageRootIds.has(nodeId)) { + return; + } + const treeId = treeIdForNode(nodeId); + const node = findUiNode(root, nodeId); + if (!treeId || !node) return; + const descendantCount = countUiNodeDescendants(node); + if (descendantCount === 0) { + onDeleteNode(treeId, nodeId); + return; + } + setPendingDelete({ + treeId, + nodeId, + nodeLabel: node.metadata.name || '未命名节点', + descendantCount, + }); + }; + + const confirmDelete = (nodeId: NodeId) => { + if (!pendingDelete || isLocked || pendingDelete.nodeId !== nodeId) { + return; + } + const { treeId } = pendingDelete; + setPendingDelete(null); + onDeleteNode(treeId, nodeId); + }; + return (
nodeId === root.id ? undefined @@ -252,9 +319,15 @@ export function UiTreePanel({ onClose={() => setContextMenu(null)} onInsertChild={(nodeId) => onInsertNode(contextTreeId, nodeId)} onInsertSibling={(nodeId) => onInsertNodeAfter(contextTreeId, nodeId)} - onDelete={(nodeId) => onDeleteNode(contextTreeId, nodeId)} + onDelete={requestDelete} /> ) : null} + setPendingDelete(null)} + onConfirm={confirmDelete} + disabled={isLocked} + />
); }