perf(*): 创作工作台 - 优化首页按需加载与画布交互并修复历史迁移冲突

This commit is contained in:
ddCat
2026-09-05 13:24:10 +08:00
parent 165b4443ab
commit 58f01d1eb6
107 changed files with 3025 additions and 1089 deletions

View File

@@ -6,7 +6,7 @@ import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface";
import { useCanvasOverlayLayer } from "@/components/canvas/canvas-overlay-layer";
import { canvasThemes } from "@/lib/canvas-theme";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport";
import { subscribeCanvasGraphicsViewportPreview, subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport";
import { useThemeStore } from "@/stores/use-theme-store";
import { CanvasNodeType, type CanvasNodeData, type ConnectionHandle, type Position, type ViewportTransform } from "@/types/canvas";
@@ -88,14 +88,8 @@ export function CanvasSelectionToolbar({ anchorRef, containerRef, count, childre
);
}
type NodePanelPlacement = "above" | "below";
// 拖动时保留当前上下方向,并允许短暂越过安全边界,避免面板在临界位置反复翻转。
const NODE_PANEL_PLACEMENT_BUFFER = 32;
export function CanvasNodePanelOverlay({ node, viewport, containerRef, panelWidth, panelHeight = 190, dragOffset, isDragging = false, children }: { node: CanvasNodeData; viewport: ViewportTransform; containerRef: RefObject<HTMLDivElement | null>; panelWidth?: number; panelHeight?: number; dragOffset?: Position | null; isDragging?: boolean; children: ReactNode }) {
const panelRef = useRef<HTMLDivElement>(null);
const placementRef = useRef<NodePanelPlacement | null>(null);
const { bringToFront, zIndex } = useCanvasOverlayLayer(`node-panel:${node.id}`, "var(--z-modal-overlay)");
const initialWidth = resolveNodePanelWidth(node, viewport, panelWidth);
const initialPosition = getNodePanelPosition(node, viewport, { width: containerRef.current?.clientWidth || 0, height: containerRef.current?.clientHeight || 0 }, initialWidth, panelHeight, dragOffset);
@@ -108,36 +102,29 @@ export function CanvasNodePanelOverlay({ node, viewport, containerRef, panelWidt
const container = containerRef.current;
const panel = panelRef.current;
if (!container || !panel) return;
let liveViewport = viewport;
let viewportSize = { width: container.clientWidth, height: container.clientHeight };
const update = (nextViewport: ViewportTransform) => {
liveViewport = nextViewport;
const nextWidth = resolveNodePanelWidth(node, nextViewport, panelWidth);
const nextHeight = panel.offsetHeight || panelHeight;
panel.style.width = `${nextWidth}px`;
panel.style.removeProperty("height");
const position = getNodePanelPosition(
node,
nextViewport,
{ width: container.clientWidth, height: container.clientHeight },
viewportSize,
nextWidth,
nextHeight,
panelHeight,
dragOffset,
isDragging ? placementRef.current : null,
isDragging ? NODE_PANEL_PLACEMENT_BUFFER : 0,
);
if (isDragging && placementRef.current && placementRef.current !== position.placement) {
panel.classList.remove("canvas-node-panel-placement-change");
// 强制重排后重新加 class,确保每次上下翻转都能重新播放虚化动画。
void panel.offsetWidth;
panel.classList.add("canvas-node-panel-placement-change");
}
placementRef.current = isDragging ? position.placement : null;
panel.style.left = `${position.left}px`;
panel.style.top = `${position.top}px`;
panel.style.transform = `translate3d(${position.left}px, ${position.top}px, 0)`;
};
update(viewport);
const resizeObserver = new ResizeObserver(() => update(viewport));
const resizeObserver = new ResizeObserver(() => {
viewportSize = { width: container.clientWidth, height: container.clientHeight };
update(liveViewport);
});
resizeObserver.observe(container);
resizeObserver.observe(panel);
const unsubscribeViewport = subscribeCanvasViewportPreview(container, update);
const unsubscribeViewport = subscribeCanvasGraphicsViewportPreview(container, update);
return () => {
resizeObserver.disconnect();
unsubscribeViewport();
@@ -148,8 +135,9 @@ export function CanvasNodePanelOverlay({ node, viewport, containerRef, panelWidt
<div
ref={panelRef}
data-canvas-no-zoom
data-canvas-node-panel
className="thin-scrollbar absolute max-w-[calc(100%_-_24px)] overflow-y-auto"
style={{ left: initialPosition.left, top: initialPosition.top, width: initialWidth, maxHeight: "calc(100% - 84px)", zIndex }}
style={{ left: 0, top: 0, transform: `translate3d(${initialPosition.left}px, ${initialPosition.top}px, 0)`, width: initialWidth, maxHeight: "calc(100% - 84px)", zIndex }}
onMouseDown={(event) => event.stopPropagation()}
onPointerDownCapture={bringToFront}
onFocusCapture={bringToFront}
@@ -257,33 +245,18 @@ function getConnectionMenuPosition(position: Position, viewport: ViewportTransfo
};
}
function getNodePanelPosition(node: CanvasNodeData, viewport: ViewportTransform, viewportSize: { width: number; height: number }, panelWidth: number, panelHeight: number, dragOffset?: Position | null, lockedPlacement?: NodePanelPlacement | null, placementBuffer = 0) {
export function getNodePanelPosition(node: CanvasNodeData, viewport: ViewportTransform, viewportSize: { width: number; height: number }, panelWidth: number, _panelHeight: number, dragOffset?: Position | null) {
const gap = 10;
const margin = 12;
const topBoundary = 72;
const offsetX = dragOffset?.x || 0;
const offsetY = dragOffset?.y || 0;
const nodeCenterX = viewport.x + (node.position.x + offsetX + node.width / 2) * viewport.k;
const nodeTop = viewport.y + (node.position.y + offsetY) * viewport.k;
const nodeBottom = viewport.y + (node.position.y + offsetY + node.height) * viewport.k;
const maxLeft = Math.max(margin, viewportSize.width - panelWidth - margin);
const left = clamp(nodeCenterX - panelWidth / 2, margin, maxLeft);
const belowTop = nodeBottom + gap;
const aboveTop = nodeTop - panelHeight - gap;
const canFitBelow = belowTop + panelHeight <= viewportSize.height - margin;
const canFitAbove = aboveTop >= topBoundary;
let placement: NodePanelPlacement;
if (lockedPlacement === "below") {
placement = !canFitBelow && belowTop + panelHeight > viewportSize.height - margin + placementBuffer && canFitAbove ? "above" : "below";
} else if (lockedPlacement === "above") {
placement = !canFitAbove && aboveTop < topBoundary - placementBuffer && canFitBelow ? "below" : "above";
} else {
placement = canFitBelow ? "below" : "above";
}
const preferredTop = placement === "below" ? belowTop : aboveTop;
return {
left,
top: clamp(preferredTop, topBoundary, Math.max(topBoundary, viewportSize.height - panelHeight - margin)),
placement,
top: nodeBottom + gap,
placement: "below" as const,
};
}