diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx
index 51b3bad50..713dd225b 100644
--- a/components/canvas/nodes/ImageNode.tsx
+++ b/components/canvas/nodes/ImageNode.tsx
@@ -69,6 +69,10 @@ import {
import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
import { buildImageProxyUrl } from "@/lib/image-delivery";
import { inferOriginalImageUrl } from "@/lib/image-url";
+import {
+ getNodeContentScale,
+ getScaledNodeContentStyle,
+} from "@/lib/canvas/node-content-scale";
import { cn } from "@/lib/utils";
import type {
CanvasTextEditPayloadItem,
@@ -350,6 +354,18 @@ interface ImageNodeProps {
export function ImageNode({ node, isSelected }: ImageNodeProps) {
const data = node.data as ImageNodeData;
+ const contentScale = useMemo(
+ () =>
+ getNodeContentScale(node.size, {
+ width: 220,
+ height: 220,
+ }),
+ [node.size.height, node.size.width]
+ );
+ const scaledContentStyle = useMemo(
+ () => getScaledNodeContentStyle(contentScale),
+ [contentScale]
+ );
const detailProgressLabel =
typeof data.metadata?.detailProgressLabel === "string"
? data.metadata.detailProgressLabel.trim()
@@ -2867,6 +2883,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
onMouseLeave={() => setIsHovered(false)}
ref={containerRef}
>
+
{/* 图片内容 - 使用 object-contain 保持原始比例 */}
{data.src ? (
@@ -3298,6 +3315,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
快照 {shortAgentExecutionSnapshotId}
) : null}
+
{/* 角度编辑对话框 */}
diff --git a/components/canvas/nodes/VideoNode.tsx b/components/canvas/nodes/VideoNode.tsx
index a20d46cfc..60c331b31 100644
--- a/components/canvas/nodes/VideoNode.tsx
+++ b/components/canvas/nodes/VideoNode.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState, useRef } from "react";
+import React, { useMemo, useRef, useState } from "react";
import { Video, Loader2, AlertCircle, MoreVertical, Download, Trash2, Play, Pause, Volume2, VolumeX } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
@@ -13,6 +13,10 @@ import {
import { Slider } from "@/components/ui/slider";
import { AgentExecutionSnapshotDialog } from "@/components/agents/AgentExecutionSnapshotDialog";
import { toast } from "sonner";
+import {
+ getNodeContentScale,
+ getScaledNodeContentStyle,
+} from "@/lib/canvas/node-content-scale";
import { useCanvasStore } from "@/store/canvas";
import type { CanvasNode, VideoNodeData } from "@/types/canvas";
import { cn } from "@/lib/utils";
@@ -24,6 +28,18 @@ interface VideoNodeProps {
export function VideoNode({ node, isSelected }: VideoNodeProps) {
const data = node.data as VideoNodeData;
+ const contentScale = useMemo(
+ () =>
+ getNodeContentScale(node.size, {
+ width: 240,
+ height: 220,
+ }),
+ [node.size.height, node.size.width]
+ );
+ const scaledContentStyle = useMemo(
+ () => getScaledNodeContentStyle(contentScale),
+ [contentScale]
+ );
const agentExecutionSnapshotId =
typeof data.metadata?.agentExecutionSnapshotId === "string"
? data.metadata.agentExecutionSnapshotId.trim()
@@ -146,6 +162,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
+
{/* 视频内容 */}
{data.src ? (
@@ -306,6 +323,7 @@ export function VideoNode({ node, isSelected }: VideoNodeProps) {
{data.prompt}
)}
+
);
}
diff --git a/components/canvas/nodes/WorkflowNode.tsx b/components/canvas/nodes/WorkflowNode.tsx
index 1cc264660..779fef9f7 100644
--- a/components/canvas/nodes/WorkflowNode.tsx
+++ b/components/canvas/nodes/WorkflowNode.tsx
@@ -13,6 +13,10 @@ import {
import { Badge } from "@/components/ui/badge";
import { useCanvasStore } from "@/store/canvas";
import { cn } from "@/lib/utils";
+import {
+ getNodeContentScale,
+ getScaledNodeContentStyle,
+} from "@/lib/canvas/node-content-scale";
import {
getVideoWorkflowNodeDefinition,
getVideoWorkflowQuickConnectOutputs,
@@ -131,6 +135,14 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
const deleteNode = useCanvasStore((state) => state.deleteNode);
const data = node.data as WorkflowNodeData;
const definition = getVideoWorkflowNodeDefinition(data.templateType);
+ const contentScale = getNodeContentScale(
+ node.size,
+ definition?.size || {
+ width: 300,
+ height: 180,
+ }
+ );
+ const scaledContentStyle = getScaledNodeContentStyle(contentScale);
const metadata = (data.metadata || {}) as Record;
const quickConnectOutputs =
data.templateType && definition
@@ -257,7 +269,8 @@ export function WorkflowNode({ node, isSelected }: WorkflowNodeProps) {
style={{ backgroundColor: accentBarColor }}
/>
-
+
+
))
- ) : (
+ ) : (
结束节点
)}
+
);
diff --git a/lib/canvas/node-content-scale.ts b/lib/canvas/node-content-scale.ts
new file mode 100644
index 000000000..f05f17e3d
--- /dev/null
+++ b/lib/canvas/node-content-scale.ts
@@ -0,0 +1,33 @@
+import type { CSSProperties } from "react";
+import type { Size } from "@/types/canvas";
+
+const DEFAULT_MIN_SCALE = 0.56;
+
+export function getNodeContentScale(
+ size: Size,
+ baseSize: Size,
+ minScale = DEFAULT_MIN_SCALE
+) {
+ if (!size.width || !size.height || !baseSize.width || !baseSize.height) {
+ return 1;
+ }
+
+ const widthScale = size.width / baseSize.width;
+ const heightScale = size.height / baseSize.height;
+ return Math.min(1, Math.max(minScale, Math.min(widthScale, heightScale)));
+}
+
+export function getScaledNodeContentStyle(
+ scale: number
+): CSSProperties | undefined {
+ if (scale >= 0.999) {
+ return undefined;
+ }
+
+ return {
+ width: `${100 / scale}%`,
+ height: `${100 / scale}%`,
+ transform: `scale(${scale})`,
+ transformOrigin: "top left",
+ };
+}