"use client"; import React, { useRef, useState, useCallback, useMemo } from "react"; import { Rnd } from "react-rnd"; import { motion } from "framer-motion"; import { useCanvasStore, canvasSelectors } from "@/store/canvas"; import type { CanvasNode, Position } from "@/types/canvas"; import { ImageNode } from "./nodes/ImageNode"; import { TextNode } from "./nodes/TextNode"; import { VideoNode } from "./nodes/VideoNode"; import { cn } from "@/lib/utils"; // 性能优化:判断节点是否在视口内 function isNodeInViewport( node: CanvasNode, viewport: { x: number; y: number; zoom: number }, containerSize: { width: number; height: number } ): boolean { const nodeScreenX = node.position.x * viewport.zoom + viewport.x; const nodeScreenY = node.position.y * viewport.zoom + viewport.y; const nodeScreenWidth = node.size.width * viewport.zoom; const nodeScreenHeight = node.size.height * viewport.zoom; // 添加缓冲区(200px),使即将进入视口的节点也被渲染 const buffer = 200; return ( nodeScreenX + nodeScreenWidth > -buffer && nodeScreenX < containerSize.width + buffer && nodeScreenY + nodeScreenHeight > -buffer && nodeScreenY < containerSize.height + buffer ); } export function CanvasWorkspace() { const containerRef = useRef(null); const [isPanning, setIsPanning] = useState(false); const [panStart, setPanStart] = useState({ x: 0, y: 0 }); const [containerSize, setContainerSize] = useState({ width: 1920, height: 1080 }); // Store state const nodes = useCanvasStore(canvasSelectors.nodes); const selectedNodeIds = useCanvasStore(canvasSelectors.selectedNodeIds); const viewport = useCanvasStore(canvasSelectors.viewport); const tool = useCanvasStore(canvasSelectors.tool); // 性能优化:监听容器大小变化 React.useEffect(() => { const updateSize = () => { if (containerRef.current) { setContainerSize({ width: containerRef.current.clientWidth, height: containerRef.current.clientHeight, }); } }; updateSize(); window.addEventListener('resize', updateSize); return () => window.removeEventListener('resize', updateSize); }, []); // 性能优化:只渲染视口内的节点(虚拟化) const visibleNodes = useMemo(() => { // 如果节点数量少于 50,直接渲染所有节点 if (nodes.length < 50) return nodes; return nodes.filter(node => isNodeInViewport(node, viewport, containerSize) || selectedNodeIds.includes(node.id) // 选中的节点总是渲染 ); }, [nodes, viewport, containerSize, selectedNodeIds]); // Store actions const setViewport = useCanvasStore((state) => state.setViewport); const selectNode = useCanvasStore((state) => state.selectNode); const clearSelection = useCanvasStore((state) => state.clearSelection); const moveNode = useCanvasStore((state) => state.moveNode); const resizeNode = useCanvasStore((state) => state.resizeNode); const pushHistory = useCanvasStore((state) => state.pushHistory); // 处理画布点击(取消选择) const handleCanvasClick = useCallback((e: React.MouseEvent) => { if (e.target === e.currentTarget) { clearSelection(); } }, [clearSelection]); // 处理画布平移开始 const handleMouseDown = useCallback((e: React.MouseEvent) => { if (tool === 'pan' || e.button === 1 || (e.button === 0 && e.altKey)) { setIsPanning(true); setPanStart({ x: e.clientX - viewport.x, y: e.clientY - viewport.y }); e.preventDefault(); } }, [tool, viewport]); // 处理画布平移 const handleMouseMove = useCallback((e: React.MouseEvent) => { if (isPanning) { setViewport({ x: e.clientX - panStart.x, y: e.clientY - panStart.y, }); } }, [isPanning, panStart, setViewport]); // 处理画布平移结束 const handleMouseUp = useCallback(() => { setIsPanning(false); }, []); // 处理滚轮缩放 const handleWheel = useCallback((e: React.WheelEvent) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; const newZoom = Math.max(0.1, Math.min(4, viewport.zoom * delta)); // 以鼠标位置为中心缩放 const rect = containerRef.current?.getBoundingClientRect(); if (rect) { const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const scaleChange = newZoom / viewport.zoom; const newX = mouseX - (mouseX - viewport.x) * scaleChange; const newY = mouseY - (mouseY - viewport.y) * scaleChange; setViewport({ x: newX, y: newY, zoom: newZoom }); } } else { // 普通滚动平移 setViewport({ x: viewport.x - e.deltaX, y: viewport.y - e.deltaY, }); } }, [viewport, setViewport]); // 渲染节点 const renderNode = (node: CanvasNode) => { const isSelected = selectedNodeIds.includes(node.id); return ( { if (!isSelected) { selectNode(node.id); } }} onDragStop={(e, d) => { const newX = (d.x - viewport.x) / viewport.zoom; const newY = (d.y - viewport.y) / viewport.zoom; moveNode(node.id, { x: newX, y: newY }); pushHistory(); }} onResizeStop={(e, direction, ref, delta, position) => { const newWidth = parseInt(ref.style.width) / viewport.zoom; const newHeight = parseInt(ref.style.height) / viewport.zoom; const newX = (position.x - viewport.x) / viewport.zoom; const newY = (position.y - viewport.y) / viewport.zoom; resizeNode(node.id, { width: newWidth, height: newHeight }); moveNode(node.id, { x: newX, y: newY }); pushHistory(); }} bounds="parent" enableResizing={isSelected && tool === 'select'} disableDragging={tool !== 'select'} className={cn( "absolute", isSelected && "ring-2 ring-primary ring-offset-2" )} style={{ overflow: 'visible' }} // 允许工具栏显示在节点外部 onClick={(e: React.MouseEvent) => { e.stopPropagation(); selectNode(node.id, e.shiftKey); }} > {node.type === 'image' && ( )} {node.type === 'text' && ( )} {node.type === 'video' && ( )} {node.type === 'message' && (
{(node.data as any).content}
)}
); }; return (
{/* 网格背景 - 浅色模式 */}
{/* 深色模式网格 */}
{/* 节点层 - 使用虚拟化渲染 */}
{visibleNodes.map(renderNode)}
{/* 性能指示器(节点数量大于 50 时显示) */} {nodes.length >= 50 && (
渲染: {visibleNodes.length}/{nodes.length}
)} {/* 空状态提示 */} {nodes.length === 0 && (

画布是空的

点击顶部"添加"按钮开始创作

)} {/* 缩放指示器 */}
{Math.round(viewport.zoom * 100)}%
); }