From 7ac1fea66200924e091e3151f646c7fb9bee27d8 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Tue, 26 May 2026 15:17:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor(canvas):=20=E9=87=8D=E6=9E=84=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=9B=BE=E5=83=8F=E8=AE=BE=E7=BD=AE=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 Portal 重新实现弹窗渲染机制 - 移除 Ant Design Popover 组件依赖 - 添加手动定位逻辑替代自动溢出调整 - 优化外部点击关闭事件处理机制 - 修复弹窗位置计算和滚动同步问题 - 简化组件参数并改进 DOM 结构 --- .../canvas-image-settings-popover.tsx | 101 +++++++++++++----- 1 file changed, 77 insertions(+), 24 deletions(-) diff --git a/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx b/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx index 957f4d7..4335658 100644 --- a/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx +++ b/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx @@ -1,8 +1,9 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type RefObject } from "react"; +import { createPortal } from "react-dom"; import { Settings2 } from "lucide-react"; -import { Button, Popover } from "antd"; +import { Button } from "antd"; import { ImageSettingsPanel, imageQualityLabel, imageSizeLabel } from "@/components/image-settings-panel"; import { canvasThemes } from "@/lib/canvas-theme"; @@ -20,10 +21,12 @@ type CanvasImageSettingsPopoverProps = { autoAdjustOverflow?: boolean; }; -export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChange, buttonClassName, getPopupContainer, placement = "topLeft", autoAdjustOverflow = true }: CanvasImageSettingsPopoverProps) { +export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChange, buttonClassName, placement = "topLeft" }: CanvasImageSettingsPopoverProps) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; - const triggerRef = useRef(null); + const buttonRef = useRef(null); + const panelRef = useRef(null); const [open, setOpen] = useState(false); + const [buttonRect, setButtonRect] = useState(null); const quality = config.quality || "auto"; const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1))); const activeSize = config.size || "auto"; @@ -34,39 +37,89 @@ export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChang useEffect(() => { if (!open) return; + const syncPosition = () => setButtonRect(buttonRef.current?.getBoundingClientRect() || null); const closeOnOutsidePointer = (event: PointerEvent) => { - const path = event.composedPath(); - if (triggerRef.current && path.includes(triggerRef.current)) return; - if (path.some((item) => item instanceof Element && item.closest(".canvas-image-settings-popover"))) return; + const target = event.target; + if (!(target instanceof Node)) return; + if (buttonRef.current?.contains(target) || panelRef.current?.contains(target)) return; setOpen(false); onOpenChange?.(false); }; + syncPosition(); + window.addEventListener("resize", syncPosition); + window.addEventListener("scroll", syncPosition, true); window.addEventListener("pointerdown", closeOnOutsidePointer, true); - return () => window.removeEventListener("pointerdown", closeOnOutsidePointer, true); + return () => { + window.removeEventListener("resize", syncPosition); + window.removeEventListener("scroll", syncPosition, true); + window.removeEventListener("pointerdown", closeOnOutsidePointer, true); + }; }, [onOpenChange, open]); + const panel = open && buttonRect ? : null; + return ( - document.body)} - onOpenChange={updateOpen} - content={ onConfigChange(key, value)} theme={theme} />} - > - - - + {panel} + + ); +} + +function ImageSettingsPortal({ + buttonRect, + panelRef, + placement, + theme, + config, + onConfigChange, +}: { + buttonRect: DOMRect; + panelRef: RefObject; + placement: CanvasImageSettingsPopoverProps["placement"]; + theme: (typeof canvasThemes)[keyof typeof canvasThemes]; + config: AiConfig; + onConfigChange: (key: keyof AiConfig, value: string) => void; +}) { + const width = 356; + const gap = 8; + const margin = 12; + const alignRight = placement?.endsWith("Right"); + const alignCenter = placement === "top" || placement === "bottom"; + const left = alignCenter ? buttonRect.left + buttonRect.width / 2 - width / 2 : alignRight ? buttonRect.right - width : buttonRect.left; + const topPlacement = placement?.startsWith("top"); + const style = { + position: "fixed", + zIndex: 1200, + width, + left: Math.max(margin, Math.min(window.innerWidth - width - margin, left)), + ...(topPlacement ? { bottom: window.innerHeight - buttonRect.top + gap, maxHeight: Math.max(260, buttonRect.top - margin * 2) } : { top: buttonRect.bottom + gap, maxHeight: Math.max(260, window.innerHeight - buttonRect.bottom - margin * 2) }), + background: theme.toolbar.panel, + borderRadius: 18, + boxShadow: "0 18px 54px rgba(28, 25, 23, 0.16)", + padding: 18, + overflowY: "auto", + color: theme.node.text, + } as const; + + return createPortal( +
event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + > + onConfigChange(key, value)} theme={theme} className="space-y-4" /> +
, + document.body, ); }