fix(canvas): 图片批次主图 - 原子同步媒体元数据并标记当前主图
This commit is contained in:
@@ -43,6 +43,7 @@ type CanvasNodeProps = {
|
||||
batchClosing?: boolean;
|
||||
batchOpening?: boolean;
|
||||
batchRecovering?: boolean;
|
||||
batchPrimary?: boolean;
|
||||
batchMotion?: { x: number; y: number; index: number };
|
||||
onMouseDown: (event: React.MouseEvent, nodeId: string) => void;
|
||||
onHoverStart: (nodeId: string) => void;
|
||||
@@ -108,6 +109,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
batchClosing = false,
|
||||
batchOpening = false,
|
||||
batchRecovering = false,
|
||||
batchPrimary = false,
|
||||
batchMotion,
|
||||
onMouseDown,
|
||||
onHoverStart,
|
||||
@@ -461,7 +463,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
{resourceLabel ? <ResourceLabelBadge reference={resourceLabel} theme={theme} /> : null}
|
||||
{hasMediaContent && !readOnly ? <ResourceStorageBadge storageKey={data.metadata?.storageKey} active={isActive} theme={theme} /> : null}
|
||||
{isBatchRoot ? <BatchToggleBadge count={batchCount} expanded={batchExpanded} theme={theme} onToggle={() => onToggleBatch?.(data.id)} /> : null}
|
||||
{isBatchChild && !readOnly ? <BatchPrimaryBadge visible={hovered || isSelected} theme={theme} onSelect={() => onSetBatchPrimary?.(data)} /> : null}
|
||||
{isBatchChild && !readOnly ? <BatchPrimaryBadge visible={batchPrimary || hovered || isSelected} selected={batchPrimary} theme={theme} onSelect={() => onSetBatchPrimary?.(data)} /> : null}
|
||||
{data.metadata?.locked ? <NodeLockBadge theme={theme} /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -802,11 +804,11 @@ function BatchToggleBadge({ count, expanded, theme, onToggle }: { count: number;
|
||||
);
|
||||
}
|
||||
|
||||
function BatchPrimaryBadge({ visible, theme, onSelect }: { visible: boolean; theme: CanvasTheme; onSelect: () => void }) {
|
||||
function BatchPrimaryBadge({ visible, selected, theme, onSelect }: { visible: boolean; selected: boolean; theme: CanvasTheme; onSelect: () => void }) {
|
||||
return (
|
||||
<button type="button" className={`canvas-node-tool-button inline-flex h-7 shrink-0 items-center gap-1 rounded-md border px-2 text-[10px] font-medium backdrop-blur-md transition-opacity ${visible ? "opacity-100" : "pointer-events-none opacity-0"}`} style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }} onClick={(event) => { event.stopPropagation(); onSelect(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<Star className="size-3" style={{ color: theme.accent.primary }} />
|
||||
主图
|
||||
<button type="button" className={`canvas-node-tool-button inline-flex h-7 shrink-0 items-center gap-1 rounded-md border px-2 text-[10px] font-medium backdrop-blur-md transition-opacity ${visible ? "opacity-100" : "pointer-events-none opacity-0"}`} style={{ background: theme.toolbar.panel, borderColor: selected ? theme.accent.primary : theme.toolbar.border, color: selected ? theme.accent.primary : theme.node.text }} aria-label={selected ? "当前主图" : "设置为主图"} aria-pressed={selected} onClick={(event) => { event.stopPropagation(); onSelect(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<Star className={`size-3 ${selected ? "fill-current" : ""}`} style={{ color: theme.accent.primary }} />
|
||||
{selected ? "当前主图" : "主图"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -296,6 +296,30 @@ export function sameNodeSemanticData(left: CanvasNodeData, right: CanvasNodeData
|
||||
return left.id === right.id && left.type === right.type && left.title === right.title && left.parentId === right.parentId && left.width === right.width && left.height === right.height && left.metadata === right.metadata;
|
||||
}
|
||||
|
||||
export function applyBatchPrimaryImage(root: CanvasNodeData, primary: CanvasNodeData): CanvasNodeData {
|
||||
// 主图是根节点对媒体的完整代理;可选字段也必须显式覆盖,避免残留上一张图的存储身份。
|
||||
return {
|
||||
...root,
|
||||
width: primary.width,
|
||||
height: primary.height,
|
||||
metadata: {
|
||||
...root.metadata,
|
||||
primaryImageId: primary.id,
|
||||
content: primary.metadata?.content,
|
||||
storageKey: primary.metadata?.storageKey,
|
||||
status: primary.metadata?.status,
|
||||
naturalWidth: primary.metadata?.naturalWidth,
|
||||
naturalHeight: primary.metadata?.naturalHeight,
|
||||
bytes: primary.metadata?.bytes,
|
||||
mimeType: primary.metadata?.mimeType,
|
||||
errorDetails: primary.metadata?.errorDetails,
|
||||
generationErrorCode: primary.metadata?.generationErrorCode,
|
||||
failedPromptFingerprint: primary.metadata?.failedPromptFingerprint,
|
||||
freeResize: primary.metadata?.freeResize,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function removeCanvasNodes(nodes: CanvasNodeData[], requestedIds: Set<string>) {
|
||||
const removedIds = new Set(requestedIds);
|
||||
nodes.forEach((node) => {
|
||||
@@ -327,17 +351,8 @@ export function removeCanvasNodes(nodes: CanvasNodeData[], requestedIds: Set<str
|
||||
if (!cleaned.metadata?.isBatchRoot || childIds?.length === cleaned.metadata.batchChildIds?.length) return cleaned;
|
||||
const primaryImageId = childIds?.includes(cleaned.metadata.primaryImageId || "") ? cleaned.metadata.primaryImageId : childIds?.[0];
|
||||
const primaryNode = remainingNodes.find((item) => item.id === primaryImageId);
|
||||
return {
|
||||
...cleaned,
|
||||
metadata: {
|
||||
...cleaned.metadata,
|
||||
batchChildIds: childIds,
|
||||
primaryImageId,
|
||||
content: primaryNode?.metadata?.content || cleaned.metadata?.content,
|
||||
naturalWidth: primaryNode?.metadata?.naturalWidth || cleaned.metadata?.naturalWidth,
|
||||
naturalHeight: primaryNode?.metadata?.naturalHeight || cleaned.metadata?.naturalHeight,
|
||||
},
|
||||
};
|
||||
const batchRoot = { ...cleaned, metadata: { ...cleaned.metadata, batchChildIds: childIds, primaryImageId } };
|
||||
return primaryNode ? applyBatchPrimaryImage(batchRoot, primaryNode) : batchRoot;
|
||||
});
|
||||
return { removedIds, nodes: nextNodes };
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ export function CanvasProjectWorldLayers(props: CanvasProjectWorldLayersProps) {
|
||||
batchClosing={Boolean(node.metadata?.batchRootId && props.collapsingBatchIds.has(node.metadata.batchRootId))}
|
||||
batchOpening={props.openingBatchIds.has(node.id)}
|
||||
batchRecovering={props.collapsingBatchIds.has(node.id)}
|
||||
batchPrimary={Boolean(node.metadata?.batchRootId && props.nodeById.get(node.metadata.batchRootId)?.metadata?.primaryImageId === node.id)}
|
||||
batchMotion={props.batchMotionById.get(node.id)}
|
||||
showImageInfo={props.showImageInfo}
|
||||
reduceMediaEffects={props.reduceMediaEffects || props.isNodeDragging || props.mediaEffectsDisabledNodeId === node.id}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { saveAs } from "file-saver";
|
||||
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { FRAME_COLLAPSED_HEIGHT, FRAME_COLLAPSED_WIDTH, getFrameChildIds, isFrameNode } from "@/lib/canvas/canvas-frame";
|
||||
import { applyNodeConfigPatch } from "@/lib/canvas/canvas-project-domain";
|
||||
import { applyBatchPrimaryImage, applyNodeConfigPatch } from "@/lib/canvas/canvas-project-domain";
|
||||
import { audioExtension, imageExtension, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error";
|
||||
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
||||
@@ -117,7 +117,7 @@ export function useCanvasNodeEditor({
|
||||
setNodes((current) =>
|
||||
current.map((node) =>
|
||||
node.id === rootId
|
||||
? { ...node, width: child.width, height: child.height, metadata: { ...node.metadata, content: child.metadata?.content, primaryImageId: child.id, naturalWidth: child.metadata?.naturalWidth, naturalHeight: child.metadata?.naturalHeight, freeResize: child.metadata?.freeResize } }
|
||||
? applyBatchPrimaryImage(node, child)
|
||||
: node,
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user