feat(canvas): 添加画布图片信息显示功能

- 在画布项目状态中添加 showImageInfo 属性
- 实现图片信息栏组件显示图片尺寸和文件大小
- 添加画布工具栏开关控制图片信息显示
- 将图片信息状态集成到项目历史记录和持久化
- 更新画布节点组件支持图片信息显示功能
This commit is contained in:
HouYunFei
2026-05-25 15:51:14 +08:00
parent 2934b1d0bc
commit 030541b99d
6 changed files with 54 additions and 38 deletions
@@ -66,6 +66,7 @@ type CanvasHistoryEntry = Pick<CanvasClipboard, "nodes" | "connections"> & {
chatSessions: CanvasAssistantSession[];
activeChatId: string | null;
backgroundMode: CanvasBackgroundMode;
showImageInfo: boolean;
};
const VIDEO_NODE_MAX_WIDTH = 420;
@@ -246,6 +247,7 @@ function InfiniteCanvasPage() {
const [runningNodeId, setRunningNodeId] = useState<string | null>(null);
const [isMiniMapOpen, setIsMiniMapOpen] = useState(false);
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>("lines");
const [showImageInfo, setShowImageInfo] = useState(false);
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
const [assetPickerOpen, setAssetPickerOpen] = useState(false);
const [assetPickerTab, setAssetPickerTab] = useState<AssetPickerTab>("my-assets");
@@ -284,8 +286,9 @@ function InfiniteCanvasPage() {
chatSessions,
activeChatId,
backgroundMode,
showImageInfo,
}),
[activeChatId, backgroundMode, chatSessions],
[activeChatId, backgroundMode, chatSessions, showImageInfo],
);
useEffect(() => {
@@ -305,6 +308,7 @@ function InfiniteCanvasPage() {
setChatSessions(restoredSessions);
setActiveChatId(project.activeChatId || null);
setBackgroundMode(project.backgroundMode);
setShowImageInfo(project.showImageInfo || false);
setViewport(project.viewport);
historyRef.current = { past: [], future: [] };
if (historyCommitTimerRef.current) {
@@ -317,6 +321,7 @@ function InfiniteCanvasPage() {
chatSessions: restoredSessions,
activeChatId: project.activeChatId || null,
backgroundMode: project.backgroundMode,
showImageInfo: project.showImageInfo || false,
};
setHistoryState({ canUndo: false, canRedo: false });
setProjectLoaded(true);
@@ -328,7 +333,7 @@ function InfiniteCanvasPage() {
if (!projectLoaded || applyingHistoryRef.current || historyPausedRef.current) return;
const next = createHistoryEntry();
const previous = lastHistoryRef.current;
if (previous?.nodes === next.nodes && previous.connections === next.connections && previous.chatSessions === next.chatSessions && previous.activeChatId === next.activeChatId && previous.backgroundMode === next.backgroundMode) return;
if (previous?.nodes === next.nodes && previous.connections === next.connections && previous.chatSessions === next.chatSessions && previous.activeChatId === next.activeChatId && previous.backgroundMode === next.backgroundMode && previous.showImageInfo === next.showImageInfo) return;
if (historyCommitTimerRef.current) clearTimeout(historyCommitTimerRef.current);
historyCommitTimerRef.current = setTimeout(() => {
@@ -348,12 +353,12 @@ function InfiniteCanvasPage() {
historyCommitTimerRef.current = null;
}
};
}, [activeChatId, backgroundMode, chatSessions, connections, createHistoryEntry, nodes, projectLoaded]);
}, [activeChatId, backgroundMode, chatSessions, connections, createHistoryEntry, nodes, projectLoaded, showImageInfo]);
useEffect(() => {
if (!projectLoaded || historyPausedRef.current) return;
updateProject(projectId, { nodes, connections, chatSessions, activeChatId, backgroundMode });
}, [activeChatId, backgroundMode, chatSessions, connections, nodes, projectId, projectLoaded, updateProject]);
updateProject(projectId, { nodes, connections, chatSessions, activeChatId, backgroundMode, showImageInfo });
}, [activeChatId, backgroundMode, chatSessions, connections, nodes, projectId, projectLoaded, showImageInfo, updateProject]);
useEffect(() => {
if (!dialogNodeId) setNodeImageSettingsOpen(false);
@@ -795,6 +800,7 @@ function InfiniteCanvasPage() {
setChatSessions(entry.chatSessions);
setActiveChatId(entry.activeChatId);
setBackgroundMode(entry.backgroundMode);
setShowImageInfo(entry.showImageInfo);
setSelectedNodeIds(new Set());
setSelectedConnectionId(null);
setContextMenu(null);
@@ -2085,6 +2091,7 @@ function InfiniteCanvasPage() {
batchOpening={openingBatchIds.has(node.id)}
batchRecovering={collapsingBatchIds.has(node.id)}
batchMotion={batchMotionById.get(node.id)}
showImageInfo={showImageInfo}
renderPanel={(panelNode) => (
<CanvasNodePromptPanel
node={panelNode}
@@ -2180,6 +2187,7 @@ function InfiniteCanvasPage() {
canUndo={historyState.canUndo}
canRedo={historyState.canRedo}
backgroundMode={backgroundMode}
showImageInfo={showImageInfo}
onAddImage={() => createNode(CanvasNodeType.Image)}
onAddVideo={() => createNode(CanvasNodeType.Video)}
onAddText={() => createNode(CanvasNodeType.Text)}
@@ -2191,6 +2199,7 @@ function InfiniteCanvasPage() {
onClear={() => setClearConfirmOpen(true)}
onDeselect={deselectCanvas}
onBackgroundModeChange={setBackgroundMode}
onShowImageInfoChange={setShowImageInfo}
onOpenAssetLibrary={() => {
setAssetPickerTab("library");
setAssetPickerOpen(true);
@@ -5,6 +5,7 @@ import type { ReactNode } from "react";
import { ChevronRight, Image as ImageIcon, RefreshCw, Star, Video } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { formatBytes } from "@/lib/image-utils";
import { useThemeStore } from "@/stores/use-theme-store";
import { CanvasNodeType, type CanvasNodeData, type Position } from "../types";
@@ -21,6 +22,7 @@ type CanvasNodeProps = {
isConnecting: boolean;
editRequestNonce?: number;
showPanel: boolean;
showImageInfo: boolean;
renderPanel?: (node: CanvasNodeData) => ReactNode;
renderNodeContent?: (node: CanvasNodeData) => ReactNode;
batchCount?: number;
@@ -71,6 +73,7 @@ export const CanvasNode = React.memo(function CanvasNode({
isConnecting,
editRequestNonce = 0,
showPanel,
showImageInfo,
renderPanel,
renderNodeContent,
batchCount = 0,
@@ -296,6 +299,8 @@ export const CanvasNode = React.memo(function CanvasNode({
/>
</div>
{showImageInfo && hasImageContent ? <ImageInfoBar node={data} /> : null}
{!hasImageContent && !hasVideoContent ? <div className="pointer-events-none absolute inset-x-0 bottom-0 h-12" style={{ background: `linear-gradient(to top, ${theme.canvas.background}66, transparent)` }} /> : null}
<ResizeHandle corner="top-left" onMouseDown={handleResizeMouseDown} />
@@ -537,6 +542,20 @@ function ImageContent({
);
}
function ImageInfoBar({ node }: { node: CanvasNodeData }) {
const width = Math.round(node.metadata?.naturalWidth || node.width);
const height = Math.round(node.metadata?.naturalHeight || node.height);
const size = formatBytes(node.metadata?.bytes || 0);
return (
<div className="pointer-events-none absolute bottom-3 right-3 z-40 max-w-[calc(100%-24px)]">
<span className="max-w-full truncate rounded-md bg-black/55 px-2 py-1 text-[11px] font-medium leading-none text-white backdrop-blur-sm">
{width} x {height}
{size ? ` · ${size}` : ""}
</span>
</div>
);
}
function BatchFrame({ batchCount, batchExpanded, batchOpening, batchRecovering, onToggleBatch, children }: { batchCount: number; batchExpanded: boolean; batchOpening: boolean; batchRecovering: boolean; onToggleBatch?: () => void; children: ReactNode }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const isBatchRoot = batchCount > 1;
@@ -1,7 +1,7 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode, RefObject } from "react";
import { useRef, useState } from "react";
import { Button, Segmented } from "antd";
import { CircleDot, Eraser, FolderOpen, Grid2x2, Hand, Image as ImageIcon, Library, Moon, Palette, Redo2, Settings2, Square, Sun, Trash2, Type, Undo2, Upload, Video } from "lucide-react";
import { Button, Segmented, Switch } from "antd";
import { CircleDot, Eraser, FolderOpen, Grid2x2, Hand, Image as ImageIcon, Info, Library, Moon, Palette, Redo2, Settings2, Square, Sun, Trash2, Type, Undo2, Upload, Video } from "lucide-react";
import { canvasThemes, type CanvasBackgroundMode, type CanvasColorTheme, type CanvasTheme } from "@/lib/canvas-theme";
import { useThemeStore } from "@/stores/use-theme-store";
@@ -12,6 +12,7 @@ export function CanvasToolbar({
canUndo,
canRedo,
backgroundMode,
showImageInfo,
onAddImage,
onAddVideo,
onAddText,
@@ -23,6 +24,7 @@ export function CanvasToolbar({
onClear,
onDeselect,
onBackgroundModeChange,
onShowImageInfoChange,
onOpenAssetLibrary,
onOpenMyAssets,
}: {
@@ -30,6 +32,7 @@ export function CanvasToolbar({
canUndo: boolean;
canRedo: boolean;
backgroundMode: CanvasBackgroundMode;
showImageInfo: boolean;
onAddImage: () => void;
onAddVideo: () => void;
onAddText: () => void;
@@ -41,6 +44,7 @@ export function CanvasToolbar({
onClear: () => void;
onDeselect: () => void;
onBackgroundModeChange: (mode: CanvasBackgroundMode) => void;
onShowImageInfoChange: (show: boolean) => void;
onOpenAssetLibrary: () => void;
onOpenMyAssets: () => void;
}) {
@@ -174,6 +178,13 @@ export function CanvasToolbar({
},
]}
/>
<div className="mt-3 flex items-center justify-between gap-3 rounded-lg px-1.5 py-1">
<span className="inline-flex min-w-0 items-center gap-1.5 text-[11px] font-medium opacity-65">
<Info className="size-3.5" />
</span>
<Switch size="small" checked={showImageInfo} onChange={onShowImageInfoChange} />
</div>
</div>
) : null}
</div>
@@ -16,6 +16,7 @@ export type CanvasProject = {
chatSessions: CanvasAssistantSession[];
activeChatId: string | null;
backgroundMode: CanvasBackgroundMode;
showImageInfo: boolean;
viewport: ViewportTransform;
};
@@ -27,7 +28,7 @@ type CanvasStore = {
openProject: (id: string) => CanvasProject | null;
renameProject: (id: string, title: string) => void;
deleteProjects: (ids: string[]) => void;
updateProject: (id: string, patch: Partial<Pick<CanvasProject, "nodes" | "connections" | "chatSessions" | "activeChatId" | "backgroundMode" | "viewport">>) => void;
updateProject: (id: string, patch: Partial<Pick<CanvasProject, "nodes" | "connections" | "chatSessions" | "activeChatId" | "backgroundMode" | "showImageInfo" | "viewport">>) => void;
};
const initialViewport: ViewportTransform = { x: 0, y: 0, k: 1 };
@@ -75,6 +76,7 @@ export const useCanvasStore = create<CanvasStore>()(
chatSessions: [],
activeChatId: null,
backgroundMode: "lines",
showImageInfo: false,
viewport: initialViewport,
};
set((state) => ({ projects: [project, ...state.projects] }));
@@ -92,6 +94,7 @@ export const useCanvasStore = create<CanvasStore>()(
chatSessions: source.chatSessions || [],
activeChatId: source.activeChatId || null,
backgroundMode: source.backgroundMode || "lines",
showImageInfo: source.showImageInfo || false,
viewport: source.viewport || initialViewport,
};
set((state) => ({ projects: [project, ...state.projects] }));