From 9c070fcfaed3016e5df9df0cf62bb8cc86e871d5 Mon Sep 17 00:00:00 2001 From: Luckyu_js <11670186+luckyu-js@user.noreply.gitee.com> Date: Fri, 17 Jul 2026 16:12:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E9=80=89=E6=97=B6=E4=B8=8D=E4=BC=9A?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=92=AD=E6=94=BE=E8=A7=86=E9=A2=91=EF=BC=8C?= =?UTF-8?q?=20=E6=A8=A1=E5=9E=8B=E8=BF=87=E6=BB=A4=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../canvas/NodeToolSelectionController.tsx | 7 +++++++ src/components/nodes/GenerateVideoNode.tsx | 9 ++++----- src/components/nodes/VideoInputNode.tsx | 7 +++---- src/store/modelStore.ts | 4 ++-- src/store/nodeToolStore.ts | 12 ++++++++++++ 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/components/canvas/NodeToolSelectionController.tsx b/src/components/canvas/NodeToolSelectionController.tsx index a721d55a..f8c9b4c7 100644 --- a/src/components/canvas/NodeToolSelectionController.tsx +++ b/src/components/canvas/NodeToolSelectionController.tsx @@ -31,6 +31,7 @@ function selectSoleSelectedNodeId(nodes: WorkflowNode[]): string | null { */ export function NodeToolSelectionController() { const setSelectionTool = useNodeToolStore((state) => state.setSelectionTool); + const setSoleSelectedNodeId = useNodeToolStore((state) => state.setSoleSelectedNodeId); const { nodeId, tool } = useWorkflowStore( useShallow((state) => { @@ -46,5 +47,11 @@ export function NodeToolSelectionController() { setSelectionTool(nodeId, tool); }, [setSelectionTool, nodeId, tool]); + // 单选 id 与派生面板分开写:前者对所有单选节点生效(如视频加载/播放), + // 后者仅在能派生 composer/高清时才有值。 + useEffect(() => { + setSoleSelectedNodeId(nodeId); + }, [setSoleSelectedNodeId, nodeId]); + return null; } diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index c6d5bb26..bb651277 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -26,7 +26,6 @@ import { formatUserFacingError } from "@/utils/userFacingErrors"; import { readVideoGenerationConfig, } from "@/utils/generationConfig"; -import { useSelectedNodeCount } from "@/hooks/useSelectedNodeCount"; import { NodeActionCapsule, type NodeActionCapsuleAction } from "./NodeActionCapsule"; import { SINGLE_INPUT_HANDLE_ID, SINGLE_OUTPUT_HANDLE_ID } from "@/utils/nodeHandles"; import { SaveToAssetLibraryModal } from "@/components/SaveToAssetLibraryModal"; @@ -399,10 +398,10 @@ export function GenerateVideoNodeView({ id, data, selected, suppressEmptyStateDe prevStatusRef.current = nodeData.status; }, [nodeData.status, displayError, t]); - const selectedNodeCount = useSelectedNodeCount(); + const isSoleSelected = useNodeToolStore((state) => state.soleSelectedNodeId === id); const activeNodeTool = useNodeToolStore((state) => getActiveNodeTool(state, id)); - const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && nodeData.outputVideo && !activeNodeTool); - const showEmptyStateDelete = Boolean(selected && selectedNodeCount === 1 && !nodeData.outputVideo && !suppressEmptyStateDelete); + const showSelectedActions = Boolean(isSoleSelected && nodeData.outputVideo && !activeNodeTool); + const showEmptyStateDelete = Boolean(isSoleSelected && !nodeData.outputVideo && !suppressEmptyStateDelete); const emptyStateActions: NodeActionCapsuleAction[] = [ { key: "delete", @@ -577,7 +576,7 @@ export function GenerateVideoNodeView({ id, data, selected, suppressEmptyStateDe key={`${nodeData.videoHistory?.[nodeData.selectedVideoHistoryIndex || 0]?.id ?? nodeData.outputVideo}-${videoRetryNonce}`} src={nodeData.outputVideo} poster={nodeData.outputVideoCover} - selected={selected} + selected={isSoleSelected} isRunning={isRunning || nodeData.status === "loading"} loop muted diff --git a/src/components/nodes/VideoInputNode.tsx b/src/components/nodes/VideoInputNode.tsx index cd875b66..1bddab8d 100644 --- a/src/components/nodes/VideoInputNode.tsx +++ b/src/components/nodes/VideoInputNode.tsx @@ -16,7 +16,6 @@ import { AssetPickerModal, PopiAssetItem } from "@/components/AssetPickerModal"; import { defaultNodeDimensions } from "@/constants/nodeDimensions"; import { uploadFileToGatewayMedia } from "@/utils/gatewayMediaUpload"; import { getPopiVideoAssetPreview } from "@/lib/popiAssetApi"; -import { useSelectedNodeCount } from "@/hooks/useSelectedNodeCount"; import { NodeActionCapsule, type NodeActionCapsuleAction } from "./NodeActionCapsule"; import { SaveToAssetLibraryModal } from "@/components/SaveToAssetLibraryModal"; import { getAutoMediaElementClassName, getAutoMediaFrameClassName } from "./mediaAutoSize"; @@ -305,9 +304,9 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro switchNodeTool(id, "videoBoxErase"); }, [id, nodeData.video, switchNodeTool]); - const selectedNodeCount = useSelectedNodeCount(); + const isSoleSelected = useNodeToolStore((state) => state.soleSelectedNodeId === id); const activeNodeTool = useNodeToolStore((state) => getActiveNodeTool(state, id)); - const showSelectedActions = Boolean(selected && selectedNodeCount === 1 && !activeNodeTool); + const showSelectedActions = Boolean(isSoleSelected && !activeNodeTool); const selectedActions: NodeActionCapsuleAction[] = hasVideoAsset ? [ { @@ -448,7 +447,7 @@ export function VideoInputNodeView({ id, data, selected }: VideoInputNodeViewPro key={`${nodeData.video ?? previewVideoPoster ?? "empty"}-${videoRetryNonce}`} src={nodeData.video} poster={previewVideoPoster} - selected={selected} + selected={isSoleSelected} isRunning={isUploading} className="absolute inset-0 h-full w-full" videoClassName={getAutoMediaElementClassName(nodeData.dimensions, "transition-opacity duration-150")} diff --git a/src/store/modelStore.ts b/src/store/modelStore.ts index 9bae9b05..534bbf74 100644 --- a/src/store/modelStore.ts +++ b/src/store/modelStore.ts @@ -80,8 +80,8 @@ function hasPopiKind(model: ProviderModel, kind: PopiModelMediaKind): boolean { function groupPopiModels(models: ProviderModel[]): Record { return { - image: models.filter((model) => !getMetadataSubTypes(model).some((subType) => subType === 107 || subType === 110 || subType === 112 || subType === 113) && hasPopiKind(model, "image")), - video: models.filter((model) => hasPopiKind(model, "video")), + image: models.filter((model) => getMetadataSubTypes(model).some((subType) => subType === 102 || subType === 103)), + video: models.filter((model) => getMetadataSubTypes(model).some((subType) => subType === 202 || subType === 203 || subType === 204)), audio: models.filter((model) => hasPopiKind(model, "audio")), "3d": models.filter((model) => hasCapability(model, ["text-to-3d", "image-to-3d"])), llm: models.filter((model) => hasPopiKind(model, "llm") || Number(model.metadata?.type) === 5), diff --git a/src/store/nodeToolStore.ts b/src/store/nodeToolStore.ts index d99ef02f..f312ef2d 100644 --- a/src/store/nodeToolStore.ts +++ b/src/store/nodeToolStore.ts @@ -57,10 +57,17 @@ interface NodeToolState { /** 当前单选节点派生出的面板(生成 composer / 高清),由选区控制器唯一写入。 */ selectionNodeId: string | null; selectionTool: SelectionToolType | null; + /** + * 当前恰好单选的节点 id(0 个或多个选中时为 null),由选区控制器唯一写入。 + * 节点用 `soleSelectedNodeId === id` 做布尔等值订阅,替代每节点 O(N²) 的 + * useSelectedNodeCount,用于「仅单选时」的行为(如视频加载/播放)。 + */ + soleSelectedNodeId: string | null; openTool: (nodeId: string, tool: NodeToolType) => void; switchTool: (nodeId: string, tool: NodeToolType) => void; setSplitGridSelection: (selection: SplitGridSelection | null) => void; setSelectionTool: (nodeId: string | null, tool: SelectionToolType | null) => void; + setSoleSelectedNodeId: (nodeId: string | null) => void; closeTool: () => void; closeToolForNode: (nodeId: string) => void; hideTool: () => void; @@ -102,6 +109,7 @@ export const useNodeToolStore = create()((set, get) => ({ splitGridSelection: null, selectionNodeId: null, selectionTool: null, + soleSelectedNodeId: null, openTool: (nodeId, tool) => set({ activeNodeId: nodeId, activeTool: tool, visible: true, splitGridSelection: null }), switchTool: (nodeId, tool) => set({ activeNodeId: nodeId, activeTool: tool, visible: true, splitGridSelection: null }), setSplitGridSelection: (selection) => set({ splitGridSelection: selection }), @@ -111,6 +119,10 @@ export const useNodeToolStore = create()((set, get) => ({ if (state.selectionNodeId === nextNodeId && state.selectionTool === tool) return; set({ selectionNodeId: nextNodeId, selectionTool: tool }); }, + setSoleSelectedNodeId: (nodeId) => { + if (get().soleSelectedNodeId === nodeId) return; + set({ soleSelectedNodeId: nodeId }); + }, // 关闭仅清理显式工具状态,保留 selection* —— 关掉 crop 等工具后,选区派生的 // composer 能凭 selectionTool 自动重新出现,无需反向重开逻辑。 closeTool: () => set({ activeNodeId: null, activeTool: null, visible: false, splitGridSelection: null }),