From 4cfb77dd44ea1571e670b6375afe38b46f7a155f Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Fri, 2 Jan 2026 01:15:04 +0800 Subject: [PATCH] fix(ui): center modals in viewport and lock background scroll --- src/components/ui/Modal.tsx | 37 ++++++++++++++++++++++++++++++++++++- src/styles/global.scss | 9 +++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index e9c7b4e..97d7a75 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback, useRef, type PropsWithChildren, type ReactNode } from 'react'; +import { createPortal } from 'react-dom'; import { IconX } from './icons'; interface ModalProps { @@ -10,6 +11,26 @@ interface ModalProps { } const CLOSE_ANIMATION_DURATION = 350; +const MODAL_LOCK_CLASS = 'modal-open'; +let activeModalCount = 0; + +const lockScroll = () => { + if (typeof document === 'undefined') return; + if (activeModalCount === 0) { + document.body?.classList.add(MODAL_LOCK_CLASS); + document.documentElement?.classList.add(MODAL_LOCK_CLASS); + } + activeModalCount += 1; +}; + +const unlockScroll = () => { + if (typeof document === 'undefined') return; + activeModalCount = Math.max(0, activeModalCount - 1); + if (activeModalCount === 0) { + document.body?.classList.remove(MODAL_LOCK_CLASS); + document.documentElement?.classList.remove(MODAL_LOCK_CLASS); + } +}; export function Modal({ open, title, onClose, footer, width = 520, children }: PropsWithChildren) { const [isVisible, setIsVisible] = useState(false); @@ -60,12 +81,20 @@ export function Modal({ open, title, onClose, footer, width = 520, children }: P }; }, []); + const shouldLockScroll = open || isVisible; + + useEffect(() => { + if (!shouldLockScroll) return; + lockScroll(); + return () => unlockScroll(); + }, [shouldLockScroll]); + if (!open && !isVisible) return null; const overlayClass = `modal-overlay ${isClosing ? 'modal-overlay-closing' : 'modal-overlay-entering'}`; const modalClass = `modal ${isClosing ? 'modal-closing' : 'modal-entering'}`; - return ( + const modalContent = (
); + + if (typeof document === 'undefined') { + return modalContent; + } + + return createPortal(modalContent, document.body); } diff --git a/src/styles/global.scss b/src/styles/global.scss index 3eb2ce7..14ed595 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -15,6 +15,15 @@ body { transition: background-color $transition-normal, color $transition-normal; } +html.modal-open, +body.modal-open { + overflow: hidden; +} + +body.modal-open .content { + overflow: hidden; +} + // 滚动条样式 ::-webkit-scrollbar { width: 8px;