feat(*): 创作与运维 - 新增图片尺寸预设和系统性能面板并优化图片组动画
This commit is contained in:
@@ -43,6 +43,7 @@ export type CanvasNodeContentProps = {
|
||||
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
||||
isBatchRoot: boolean;
|
||||
batchCount: number;
|
||||
batchPreviewNodes?: CanvasNodeData[];
|
||||
batchExpanded: boolean;
|
||||
batchOpening: boolean;
|
||||
batchRecovering: boolean;
|
||||
@@ -398,13 +399,13 @@ function ImageNodeContent(props: CanvasNodeContentProps) {
|
||||
: props.node.metadata?.status === "error"
|
||||
? <ErrorContent node={props.node} theme={props.theme} onRetry={props.onRetry} onReloadResource={props.onReloadResource} />
|
||||
: <EmptyImageContent {...props} isBatchRoot={false} />;
|
||||
return <BatchFrame batchCount={props.batchCount} batchExpanded={props.batchExpanded} batchOpening={props.batchOpening} batchRecovering={props.batchRecovering} theme={props.theme} onToggleBatch={props.onToggleBatch}>{content}</BatchFrame>;
|
||||
return <BatchFrame batchPreviewNodes={props.batchPreviewNodes} batchCount={props.batchCount} batchExpanded={props.batchExpanded} batchOpening={props.batchOpening} batchRecovering={props.batchRecovering} theme={props.theme} onToggleBatch={props.onToggleBatch}>{content}</BatchFrame>;
|
||||
}
|
||||
if (!props.node.metadata?.content) return <EmptyImageContent {...props} />;
|
||||
return <ImageContent node={props.node} theme={props.theme} isBatchRoot={props.isBatchRoot} batchCount={props.batchCount} batchExpanded={props.batchExpanded} batchOpening={props.batchOpening} batchRecovering={props.batchRecovering} onToggleBatch={props.onToggleBatch} />;
|
||||
return <ImageContent batchPreviewNodes={props.batchPreviewNodes} node={props.node} theme={props.theme} isBatchRoot={props.isBatchRoot} batchCount={props.batchCount} batchExpanded={props.batchExpanded} batchOpening={props.batchOpening} batchRecovering={props.batchRecovering} onToggleBatch={props.onToggleBatch} />;
|
||||
}
|
||||
|
||||
function EmptyImageContent({ node, theme, isBatchRoot, batchCount, batchExpanded, batchOpening, batchRecovering, onToggleBatch }: CanvasNodeContentProps) {
|
||||
function EmptyImageContent({ node, theme, isBatchRoot, batchCount, batchPreviewNodes, batchExpanded, batchOpening, batchRecovering, onToggleBatch }: CanvasNodeContentProps) {
|
||||
const isCharacterReference = node.metadata?.workflowKind === "character" && node.metadata?.characterView === "multi";
|
||||
const content = (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-3" style={{ color: theme.node.placeholder }}>
|
||||
@@ -417,7 +418,7 @@ function EmptyImageContent({ node, theme, isBatchRoot, batchCount, batchExpanded
|
||||
) : <span className="text-[var(--fs-tiny)] tracking-[0.18em] opacity-50">空图片节点</span>}
|
||||
</div>
|
||||
);
|
||||
if (isBatchRoot) return <BatchFrame batchCount={batchCount} batchExpanded={batchExpanded} batchOpening={batchOpening} batchRecovering={batchRecovering} theme={theme} onToggleBatch={onToggleBatch}>{content}</BatchFrame>;
|
||||
if (isBatchRoot) return <BatchFrame batchPreviewNodes={batchPreviewNodes} batchCount={batchCount} batchExpanded={batchExpanded} batchOpening={batchOpening} batchRecovering={batchRecovering} theme={theme} onToggleBatch={onToggleBatch}>{content}</BatchFrame>;
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -571,7 +572,7 @@ function EmptyMediaContent({ icon, label, color }: { icon: ReactNode; label: str
|
||||
return <div className="flex h-full w-full flex-col items-center justify-center gap-3" style={{ color }}>{icon}<span className="text-sm">{label}</span></div>;
|
||||
}
|
||||
|
||||
function ImageContent({ node, theme, isBatchRoot, batchCount, batchExpanded, batchOpening, batchRecovering, onToggleBatch }: Pick<CanvasNodeContentProps, "node" | "theme" | "isBatchRoot" | "batchCount" | "batchExpanded" | "batchOpening" | "batchRecovering" | "onToggleBatch">) {
|
||||
function ImageContent({ node, theme, isBatchRoot, batchCount, batchPreviewNodes, batchExpanded, batchOpening, batchRecovering, onToggleBatch }: Pick<CanvasNodeContentProps, "node" | "theme" | "isBatchRoot" | "batchCount" | "batchPreviewNodes" | "batchExpanded" | "batchOpening" | "batchRecovering" | "onToggleBatch">) {
|
||||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
||||
const nearViewport = useNearViewport(imageContainerRef);
|
||||
const { url, loading } = useNodeResourceUrl(node, nearViewport);
|
||||
@@ -615,7 +616,7 @@ function ImageContent({ node, theme, isBatchRoot, batchCount, batchExpanded, bat
|
||||
};
|
||||
|
||||
return (
|
||||
<BatchFrame batchCount={isBatchRoot ? batchCount : 0} batchExpanded={batchExpanded} batchOpening={batchOpening} batchRecovering={batchRecovering} theme={theme} onToggleBatch={onToggleBatch}>
|
||||
<BatchFrame batchPreviewNodes={batchPreviewNodes} batchCount={isBatchRoot ? batchCount : 0} batchExpanded={batchExpanded} batchOpening={batchOpening} batchRecovering={batchRecovering} theme={theme} onToggleBatch={onToggleBatch}>
|
||||
<div ref={imageContainerRef} className="h-full w-full overflow-hidden rounded-[var(--node-radius)]">
|
||||
{url ? <img src={url} alt={node.title} loading="lazy" decoding="async" draggable={false} onDragStart={(event) => event.preventDefault()} onLoad={(event) => fitToImage(event.currentTarget)} className={`pointer-events-none block h-full w-full select-none ${node.metadata?.freeResize ? "object-fill" : "object-contain"}`} /> : <div className="grid size-full place-items-center" style={{ color: theme.node.muted }}>{loading ? <LoaderCircle className="size-5 animate-spin" /> : <ImageIcon className="size-5 opacity-45" />}</div>}
|
||||
</div>
|
||||
@@ -695,7 +696,14 @@ export function CanvasNodeImageInfo({ node }: { node: CanvasNodeData }) {
|
||||
return <span className="ml-auto max-w-full shrink-0 truncate rounded-[var(--r-sm)] bg-black/55 px-2 py-1 text-[var(--fs-label)] font-medium leading-none text-white backdrop-blur-sm">{width} x {height}{size ? ` · ${size}` : ""}</span>;
|
||||
}
|
||||
|
||||
function BatchFrame({ batchCount, batchExpanded, batchOpening, batchRecovering, theme, onToggleBatch, children }: { batchCount: number; batchExpanded: boolean; batchOpening: boolean; batchRecovering: boolean; theme: CanvasTheme; onToggleBatch?: () => void; children: ReactNode }) {
|
||||
function BatchPreviewImage({ node }: { node: CanvasNodeData }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const nearViewport = useNearViewport(ref);
|
||||
const { url } = useNodeResourceUrl(node, nearViewport);
|
||||
return <div ref={ref} className="h-full w-full overflow-hidden rounded-[inherit]">{url ? <img src={url} alt={`子图预览:${node.title}`} className="h-full w-full object-contain" draggable={false} /> : null}</div>;
|
||||
}
|
||||
|
||||
function BatchFrame({ batchCount, batchPreviewNodes, batchExpanded, batchOpening, batchRecovering, theme, onToggleBatch, children }: { batchCount: number; batchPreviewNodes?: CanvasNodeData[]; batchExpanded: boolean; batchOpening: boolean; batchRecovering: boolean; theme: CanvasTheme; onToggleBatch?: () => void; children: ReactNode }) {
|
||||
const isBatchRoot = batchCount > 1;
|
||||
return (
|
||||
<div className="group/batch relative h-full w-full overflow-visible" onDoubleClick={isBatchRoot ? (event) => { event.stopPropagation(); onToggleBatch?.(); } : undefined}>
|
||||
@@ -704,7 +712,7 @@ function BatchFrame({ batchCount, batchExpanded, batchOpening, batchRecovering,
|
||||
{Array.from({ length: Math.min(batchCount - 1, 5) }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="absolute rounded-[inherit] transition-all duration-300 group-hover/batch:translate-x-2"
|
||||
className="absolute rounded-[var(--r-lg)] transition-all duration-300 group-hover/batch:translate-x-2"
|
||||
style={{
|
||||
inset: 0,
|
||||
background: theme.node.panel,
|
||||
@@ -713,7 +721,9 @@ function BatchFrame({ batchCount, batchExpanded, batchOpening, batchRecovering,
|
||||
transform: batchOpening || batchRecovering ? `translate(${54 + index * 22}px, ${20 + index * 12}px) rotate(${8 + index * 5}deg) scale(.98)` : `translate(${34 + index * 18}px, ${14 + index * 10}px) rotate(${6 + index * 4}deg)`,
|
||||
zIndex: -index - 1,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{batchPreviewNodes?.[index] ? <BatchPreviewImage node={batchPreviewNodes[index]} /> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -37,6 +37,7 @@ type CanvasNodeProps = {
|
||||
renderNodeContent?: (node: CanvasNodeData) => ReactNode;
|
||||
drawingProjectId?: string;
|
||||
batchCount?: number;
|
||||
batchPreviewNodes?: CanvasNodeData[];
|
||||
batchExpanded?: boolean;
|
||||
batchClosing?: boolean;
|
||||
batchOpening?: boolean;
|
||||
@@ -83,6 +84,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
renderNodeContent,
|
||||
drawingProjectId,
|
||||
batchCount = 0,
|
||||
batchPreviewNodes,
|
||||
batchExpanded = false,
|
||||
batchClosing = false,
|
||||
batchOpening = false,
|
||||
@@ -347,8 +349,8 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
"--batch-from-x": `${batchMotion?.x || 0}px`,
|
||||
"--batch-from-y": `${batchMotion?.y || 0}px`,
|
||||
"--batch-from-rotate": `${6 + (batchMotion?.index || 0) * 4}deg`,
|
||||
animation: data.metadata?.batchRootId ? (batchClosing ? `canvas-batch-child-out var(--motion-dur-base-calc) var(--motion-ease-in-out) both` : `canvas-batch-child-in var(--motion-dur-slow-calc) var(--motion-ease-out) both`) : undefined,
|
||||
animationDelay: data.metadata?.batchRootId ? `${batchClosing ? 0 : 45 + (batchMotion?.index || 0) * 24}ms` : undefined,
|
||||
animation: isBatchChild && (batchClosing || batchOpening) ? (batchClosing ? `canvas-batch-child-out var(--motion-dur-base-calc) var(--motion-ease-in-out) both` : `canvas-batch-child-in var(--motion-dur-slow-calc) var(--motion-ease-out) both`) : undefined,
|
||||
animationDelay: isBatchChild && (batchClosing || batchOpening) ? `${batchClosing ? 0 : 45 + (batchMotion?.index || 0) * 24}ms` : undefined,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
@@ -363,6 +365,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
textareaRef={textareaRef}
|
||||
isBatchRoot={isBatchRoot}
|
||||
batchCount={batchCount}
|
||||
batchPreviewNodes={batchPreviewNodes}
|
||||
batchExpanded={batchExpanded}
|
||||
batchOpening={batchOpening}
|
||||
batchRecovering={batchRecovering}
|
||||
@@ -494,6 +497,7 @@ function areCanvasNodePropsEqual(previous: CanvasNodeProps, next: CanvasNodeProp
|
||||
previous.renderNodeContent === next.renderNodeContent &&
|
||||
previous.drawingProjectId === next.drawingProjectId &&
|
||||
previous.batchCount === next.batchCount &&
|
||||
previous.batchPreviewNodes === next.batchPreviewNodes &&
|
||||
previous.batchExpanded === next.batchExpanded &&
|
||||
previous.batchClosing === next.batchClosing &&
|
||||
previous.batchOpening === next.batchOpening &&
|
||||
@@ -548,7 +552,7 @@ function NodeLockBadge({ theme }: { theme: CanvasTheme }) {
|
||||
|
||||
function BatchToggleBadge({ count, expanded, theme, onToggle }: { count: number; expanded: boolean; theme: CanvasTheme; onToggle: () => 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-[var(--fs-tiny)] font-semibold backdrop-blur-md" style={{ background: `${theme.toolbar.panel}d9`, borderColor: `${theme.toolbar.border}cc`, color: theme.node.text }} aria-label={expanded ? "图片组已展开" : "图片组已收起"} onClick={(event) => { event.stopPropagation(); onToggle(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<button type="button" className="canvas-node-tool-button inline-flex h-7 shrink-0 items-center gap-1 rounded-md border px-2 text-[var(--fs-tiny)] font-semibold backdrop-blur-md" style={{ background: `${theme.toolbar.panel}d9`, borderColor: `${theme.toolbar.border}cc`, color: theme.node.text }} aria-expanded={expanded} aria-label={expanded ? "收起图片组" : "展开图片组"} onClick={(event) => { event.stopPropagation(); onToggle(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<span className="leading-none" style={{ color: theme.accent.primary }}>{count}</span>
|
||||
<ChevronRight className={`size-3 opacity-55 transition-transform ${expanded ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
|
||||
@@ -158,7 +158,7 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co
|
||||
const reducedMotion = useReducedMotion();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { bringToFront, zIndex } = useCanvasOverlayLayer("connection-create-menu", "var(--z-modal-overlay)");
|
||||
const menuWidth = 248;
|
||||
const menuWidth = Math.min(288, viewportSize.width - 24);
|
||||
const menuHeight = canCreateDrawing ? 456 : 412;
|
||||
const gap = 12;
|
||||
const initialPosition = getConnectionMenuPosition(pending.position, viewport, viewportSize, menuWidth, menuHeight, gap);
|
||||
@@ -188,10 +188,10 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co
|
||||
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 6, scale: 0.97, rotateX: 2 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1, rotateX: 0 }}
|
||||
transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }}
|
||||
className="aceternity-floating-panel absolute w-[248px] origin-top-left overflow-hidden rounded-[var(--r-2xl)] border p-2 backdrop-blur-2xl"
|
||||
className="aceternity-floating-panel absolute origin-top-left overflow-x-hidden overflow-y-auto rounded-[var(--r-2xl)] border p-2 backdrop-blur-2xl"
|
||||
data-canvas-no-zoom
|
||||
data-connection-create-menu
|
||||
style={{ left: initialPosition.left, top: initialPosition.top, zIndex, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text }}
|
||||
style={{ width: menuWidth, maxHeight: Math.max(120, viewportSize.height - 84), left: initialPosition.left, top: initialPosition.top, zIndex, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDownCapture={bringToFront}
|
||||
onFocusCapture={bringToFront}
|
||||
@@ -205,7 +205,7 @@ export function CanvasConnectionCreateMenu({ pending, viewport, viewportSize, co
|
||||
</span>
|
||||
<button type="button" className="grid size-6 shrink-0 place-items-center rounded-full border opacity-55 transition-opacity hover:opacity-100" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }} onClick={onClose} aria-label="关闭连线创建菜单"><X className="size-3" /></button>
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<div className="grid min-w-0 grid-cols-1 gap-1">
|
||||
<ConnectionCreateOption motionEnabled={!reducedMotion} icon={<List className="size-4" />} title="文本生成" disabledReason={getDisabledReason(CanvasNodeType.Text)} onClick={() => onCreate(CanvasNodeType.Text)} />
|
||||
<ConnectionCreateOption motionEnabled={!reducedMotion} icon={<Clapperboard className="size-4" />} title="分镜脚本" disabledReason={getDisabledReason(CanvasNodeType.Script)} onClick={() => onCreate(CanvasNodeType.Script)} />
|
||||
<ConnectionCreateOption motionEnabled={!reducedMotion} icon={<ImageIcon className="size-4" />} title="图片生成" disabledReason={getDisabledReason(CanvasNodeType.Image)} onClick={() => onCreate(CanvasNodeType.Image)} />
|
||||
@@ -225,7 +225,7 @@ function ConnectionCreateOption({ motionEnabled, icon, title, description, disab
|
||||
<span className="grid size-7 shrink-0 place-items-center rounded-[var(--r-md)] opacity-65 transition-opacity group-hover:opacity-100 [&_svg]:size-3.5" style={{ background: theme.toolbar.itemHover }}>{icon}</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-2 text-[var(--fs-tiny)] font-semibold leading-4">{title}</span>
|
||||
{disabledReason || description ? <span className="mt-0.5 block truncate text-[var(--fs-micro)]" style={{ color: theme.node.muted }}>{disabledReason || description}</span> : null}
|
||||
{disabledReason || description ? <span className="mt-0.5 block whitespace-normal break-words text-[var(--fs-micro)] leading-relaxed" style={{ color: theme.node.muted }}>{disabledReason || description}</span> : null}
|
||||
</span>
|
||||
<ChevronRight className="size-3.5 shrink-0 opacity-35 transition-transform group-hover:translate-x-0.5" />
|
||||
</motion.button>
|
||||
|
||||
Reference in New Issue
Block a user