event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
+ {isEditingTitle ? (
+ setTitleDraft(event.target.value)}
+ onBlur={finishTitleEditing}
+ onKeyDown={(event) => {
+ if (event.key === "Enter") finishTitleEditing();
+ if (event.key === "Escape") {
+ setTitleDraft(data.title || "");
+ setIsEditingTitle(false);
+ }
+ }}
+ />
+ ) : (
+
+ )}
+
+
-
{node.title || "组"}
+
组
{groupChildCount} 个节点
@@ -547,7 +614,7 @@ function AudioNodeContent({ node, theme }: NodeContentRendererProps) {
- {node.title || "音频"}
+ 音频
diff --git a/web/src/constant/canvas.ts b/web/src/constant/canvas.ts
index e13eec7..2bb7420 100644
--- a/web/src/constant/canvas.ts
+++ b/web/src/constant/canvas.ts
@@ -9,11 +9,11 @@ type CanvasNodeSpec = {
};
export const NODE_DEFAULT_SIZE = {
- [CanvasNodeType.Image]: { width: 340, height: 240, title: "New Generation" },
- [CanvasNodeType.Text]: { width: 340, height: 240, title: "Note" },
+ [CanvasNodeType.Image]: { width: 340, height: 240, title: "图片" },
+ [CanvasNodeType.Text]: { width: 340, height: 240, title: "文本" },
[CanvasNodeType.Config]: { width: 340, height: 240, title: "生成配置" },
- [CanvasNodeType.Video]: { width: 420, height: 236, title: "Video" },
- [CanvasNodeType.Audio]: { width: 340, height: 120, title: "Audio" },
+ [CanvasNodeType.Video]: { width: 420, height: 236, title: "视频" },
+ [CanvasNodeType.Audio]: { width: 340, height: 120, title: "音频" },
[CanvasNodeType.Group]: { width: 760, height: 480, title: "组" },
} satisfies Record
;
diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx
index 96a1d02..6c550cb 100644
--- a/web/src/pages/canvas/project.tsx
+++ b/web/src/pages/canvas/project.tsx
@@ -235,7 +235,6 @@ function InfiniteCanvasPage() {
const historyPausedRef = useRef(false);
const didInitialCenterRef = useRef(false);
const rafRef = useRef(null);
- const toolbarHideTimerRef = useRef | null>(null);
const nodeDraggingRef = useRef(false);
const dragRef = useRef<{
isDraggingNode: boolean;
@@ -547,25 +546,12 @@ function InfiniteCanvasPage() {
}
}, []);
- const keepNodeToolbar = useCallback(
- (nodeId: string) => {
- if (nodeDraggingRef.current || nodeImageSettingsOpen) return;
- if (toolbarHideTimerRef.current) {
- clearTimeout(toolbarHideTimerRef.current);
- toolbarHideTimerRef.current = null;
- }
- setToolbarNodeId(nodeId);
- },
- [nodeImageSettingsOpen],
- );
+ const keepNodeToolbar = useCallback((nodeId: string) => {
+ if (nodeDraggingRef.current || nodeImageSettingsOpen || !selectedNodeIdsRef.current.has(nodeId)) return;
+ setToolbarNodeId(nodeId);
+ }, [nodeImageSettingsOpen]);
- const hideNodeToolbar = useCallback(() => {
- if (toolbarHideTimerRef.current) clearTimeout(toolbarHideTimerRef.current);
- toolbarHideTimerRef.current = setTimeout(() => {
- setToolbarNodeId(null);
- toolbarHideTimerRef.current = null;
- }, 120);
- }, []);
+ const hideNodeToolbar = useCallback(() => {}, []);
const connectNodes = useCallback(
(current: ConnectionHandle, targetNodeId: string) => {
@@ -1106,6 +1092,7 @@ function InfiniteCanvasPage() {
}
setSelectedNodeIds(nextSelected);
+ setToolbarNodeId(nextSelected.size === 1 && nextSelected.has(nodeId) ? nodeId : null);
const dragIds = new Set(nextSelected);
currentNodes.forEach((node) => {
if (!nextSelected.has(node.id)) return;
@@ -1502,6 +1489,10 @@ function InfiniteCanvasPage() {
setNodes((prev) => prev.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, content } } : node)));
}, []);
+ const handleNodeTitleChange = useCallback((nodeId: string, title: string) => {
+ setNodes((prev) => prev.map((node) => (node.id === nodeId ? { ...node, title } : node)));
+ }, []);
+
const toggleBatchExpanded = useCallback((nodeId: string) => {
const isExpanded = Boolean(nodesRef.current.find((node) => node.id === nodeId)?.metadata?.imageBatchExpanded);
if (isExpanded) {
@@ -2658,15 +2649,14 @@ function InfiniteCanvasPage() {
onHoverStart={(nodeId) => {
if (nodeDraggingRef.current) return;
setHoveredNodeId(nodeId);
- keepNodeToolbar(nodeId);
}}
onHoverEnd={(nodeId) => {
setHoveredNodeId((current) => (current === nodeId ? null : current));
- hideNodeToolbar();
}}
onConnectStart={handleConnectStart}
onResize={handleNodeResize}
onContentChange={handleNodeContentChange}
+ onTitleChange={handleNodeTitleChange}
onToggleBatch={toggleBatchExpanded}
onSetBatchPrimary={setBatchPrimary}
onRetry={(node) => void handleRetryNode(node)}