@@ -251,6 +251,7 @@ function InfiniteCanvasPage() {
|
|||||||
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
|
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
|
||||||
const [cropNodeId, setCropNodeId] = useState<string | null>(null);
|
const [cropNodeId, setCropNodeId] = useState<string | null>(null);
|
||||||
const [angleNodeId, setAngleNodeId] = useState<string | null>(null);
|
const [angleNodeId, setAngleNodeId] = useState<string | null>(null);
|
||||||
|
const [previewNodeId, setPreviewNodeId] = useState<string | null>(null);
|
||||||
const [assistantCollapsed, setAssistantCollapsed] = useState(true);
|
const [assistantCollapsed, setAssistantCollapsed] = useState(true);
|
||||||
const [assistantMounted, setAssistantMounted] = useState(false);
|
const [assistantMounted, setAssistantMounted] = useState(false);
|
||||||
const [titleEditing, setTitleEditing] = useState(false);
|
const [titleEditing, setTitleEditing] = useState(false);
|
||||||
@@ -525,6 +526,7 @@ function InfiniteCanvasPage() {
|
|||||||
const infoNode = infoNodeId ? nodeById.get(infoNodeId) || null : null;
|
const infoNode = infoNodeId ? nodeById.get(infoNodeId) || null : null;
|
||||||
const cropNode = cropNodeId ? nodeById.get(cropNodeId) || null : null;
|
const cropNode = cropNodeId ? nodeById.get(cropNodeId) || null : null;
|
||||||
const angleNode = angleNodeId ? nodeById.get(angleNodeId) || null : null;
|
const angleNode = angleNodeId ? nodeById.get(angleNodeId) || null : null;
|
||||||
|
const previewNode = previewNodeId ? nodeById.get(previewNodeId) || null : null;
|
||||||
const hasMultipleSelectedNodes = selectedNodeIds.size > 1;
|
const hasMultipleSelectedNodes = selectedNodeIds.size > 1;
|
||||||
const activeNodeId = hasMultipleSelectedNodes ? null : hoveredNodeId || (selectedNodeIds.size === 1 ? Array.from(selectedNodeIds)[0] : null);
|
const activeNodeId = hasMultipleSelectedNodes ? null : hoveredNodeId || (selectedNodeIds.size === 1 ? Array.from(selectedNodeIds)[0] : null);
|
||||||
const batchChildCountById = useMemo(() => {
|
const batchChildCountById = useMemo(() => {
|
||||||
@@ -631,6 +633,7 @@ function InfiniteCanvasPage() {
|
|||||||
setInfoNodeId((current) => (current && allIds.has(current) ? null : current));
|
setInfoNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||||
setCropNodeId((current) => (current && allIds.has(current) ? null : current));
|
setCropNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||||
setAngleNodeId((current) => (current && allIds.has(current) ? null : current));
|
setAngleNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||||
|
setPreviewNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||||
setRunningNodeId((current) => (current && allIds.has(current) ? null : current));
|
setRunningNodeId((current) => (current && allIds.has(current) ? null : current));
|
||||||
setContextMenu((current) => (current && allIds.has(current.nodeId) ? null : current));
|
setContextMenu((current) => (current && allIds.has(current.nodeId) ? null : current));
|
||||||
cleanupAssetImages({ projectId, nodes: nodesRef.current.filter((node) => !allIds.has(node.id)), chatSessions });
|
cleanupAssetImages({ projectId, nodes: nodesRef.current.filter((node) => !allIds.has(node.id)), chatSessions });
|
||||||
@@ -656,6 +659,7 @@ function InfiniteCanvasPage() {
|
|||||||
setInfoNodeId(null);
|
setInfoNodeId(null);
|
||||||
setCropNodeId(null);
|
setCropNodeId(null);
|
||||||
setAngleNodeId(null);
|
setAngleNodeId(null);
|
||||||
|
setPreviewNodeId(null);
|
||||||
setRunningNodeId(null);
|
setRunningNodeId(null);
|
||||||
deselectCanvas();
|
deselectCanvas();
|
||||||
setClearConfirmOpen(false);
|
setClearConfirmOpen(false);
|
||||||
@@ -2084,6 +2088,7 @@ function InfiniteCanvasPage() {
|
|||||||
onSaveAsset={(node) => void saveNodeAsset(node)}
|
onSaveAsset={(node) => void saveNodeAsset(node)}
|
||||||
onCrop={(node) => setCropNodeId(node.id)}
|
onCrop={(node) => setCropNodeId(node.id)}
|
||||||
onAngle={(node) => setAngleNodeId(node.id)}
|
onAngle={(node) => setAngleNodeId(node.id)}
|
||||||
|
onViewImage={(node) => setPreviewNodeId(node.id)}
|
||||||
onRetry={(node) => void handleRetryNode(node)}
|
onRetry={(node) => void handleRetryNode(node)}
|
||||||
onToggleFreeResize={(node) => toggleNodeFreeResize(node.id)}
|
onToggleFreeResize={(node) => toggleNodeFreeResize(node.id)}
|
||||||
onDelete={(node) => deleteNodes(new Set([node.id]))}
|
onDelete={(node) => deleteNodes(new Set([node.id]))}
|
||||||
@@ -2141,6 +2146,24 @@ function InfiniteCanvasPage() {
|
|||||||
|
|
||||||
{angleNode?.metadata?.content ? <CanvasNodeAngleDialog dataUrl={angleNode.metadata.content} open={Boolean(angleNode)} onClose={() => setAngleNodeId(null)} onConfirm={(params) => void generateAngleNode(angleNode!, params)} /> : null}
|
{angleNode?.metadata?.content ? <CanvasNodeAngleDialog dataUrl={angleNode.metadata.content} open={Boolean(angleNode)} onClose={() => setAngleNodeId(null)} onConfirm={(params) => void generateAngleNode(angleNode!, params)} /> : null}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="图片详情"
|
||||||
|
open={Boolean(previewNode?.metadata?.content)}
|
||||||
|
centered
|
||||||
|
onCancel={() => setPreviewNodeId(null)}
|
||||||
|
footer={null}
|
||||||
|
width="auto"
|
||||||
|
styles={{ body: { padding: 0, display: "flex", justifyContent: "center", alignItems: "center", maxHeight: "80vh" } }}
|
||||||
|
>
|
||||||
|
{previewNode?.metadata?.content ? (
|
||||||
|
<img
|
||||||
|
src={previewNode.metadata.content}
|
||||||
|
alt={previewNode.title || "图片"}
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "80vh", objectFit: "contain" }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="清空画布?"
|
title="清空画布?"
|
||||||
open={clearConfirmOpen}
|
open={clearConfirmOpen}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import { Modal, Segmented, Tooltip } from "antd";
|
import { Modal, Segmented, Tooltip } from "antd";
|
||||||
import { Camera, Download, FolderPlus, Image as ImageIcon, Info, Lock, LockOpen, MessageSquare, Minus, Pencil, Plus, RefreshCw, Scissors, Settings2, Trash2, Upload } from "lucide-react";
|
import { Camera, Download, FolderPlus, Image as ImageIcon, Info, Lock, LockOpen, Maximize2, MessageSquare, Minus, Pencil, Plus, RefreshCw, Scissors, Settings2, Trash2, Upload } from "lucide-react";
|
||||||
|
|
||||||
import { canvasThemes } from "@/lib/canvas-theme";
|
import { canvasThemes } from "@/lib/canvas-theme";
|
||||||
import { formatBytes, getDataUrlByteSize } from "@/lib/image-utils";
|
import { formatBytes, getDataUrlByteSize } from "@/lib/image-utils";
|
||||||
@@ -25,6 +25,7 @@ type CanvasNodeHoverToolbarProps = {
|
|||||||
onSaveAsset: (node: CanvasNodeData) => void;
|
onSaveAsset: (node: CanvasNodeData) => void;
|
||||||
onCrop: (node: CanvasNodeData) => void;
|
onCrop: (node: CanvasNodeData) => void;
|
||||||
onAngle: (node: CanvasNodeData) => void;
|
onAngle: (node: CanvasNodeData) => void;
|
||||||
|
onViewImage: (node: CanvasNodeData) => void;
|
||||||
onRetry: (node: CanvasNodeData) => void;
|
onRetry: (node: CanvasNodeData) => void;
|
||||||
onToggleFreeResize: (node: CanvasNodeData) => void;
|
onToggleFreeResize: (node: CanvasNodeData) => void;
|
||||||
onDelete: (node: CanvasNodeData) => void;
|
onDelete: (node: CanvasNodeData) => void;
|
||||||
@@ -46,6 +47,7 @@ export function CanvasNodeHoverToolbar({
|
|||||||
onSaveAsset,
|
onSaveAsset,
|
||||||
onCrop,
|
onCrop,
|
||||||
onAngle,
|
onAngle,
|
||||||
|
onViewImage,
|
||||||
onRetry,
|
onRetry,
|
||||||
onToggleFreeResize,
|
onToggleFreeResize,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -95,6 +97,7 @@ export function CanvasNodeHoverToolbar({
|
|||||||
) : null}
|
) : null}
|
||||||
{hasImage ? <ToolbarAction title="裁剪并生成新节点" label="裁剪" icon={<Scissors className="size-4" />} onClick={() => onCrop(node)} /> : null}
|
{hasImage ? <ToolbarAction title="裁剪并生成新节点" label="裁剪" icon={<Scissors className="size-4" />} onClick={() => onCrop(node)} /> : null}
|
||||||
{hasImage ? <ToolbarAction title="生成角度" label="多角度" icon={<Camera className="size-4" />} onClick={() => onAngle(node)} /> : null}
|
{hasImage ? <ToolbarAction title="生成角度" label="多角度" icon={<Camera className="size-4" />} onClick={() => onAngle(node)} /> : null}
|
||||||
|
{hasImage ? <ToolbarAction title="查看图片详情" label="查看大图" icon={<Maximize2 className="size-4" />} onClick={() => onViewImage(node)} /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user