You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让弹窗始终精准定位在日历事件元素正上方?

解决FullCalendar事件弹窗精准定位的问题

针对你遇到的弹窗在不同页面尺寸下偏移的问题,核心原因是直接依赖元素物理位置时没有处理滚动偏移、窗口resize后的位置变化,以及弹窗自身尺寸的居中计算。下面提供两种稳定的解决方案:

方案一:基于视口坐标的绝对定位(通用型)

通过获取事件元素的视口位置,结合滚动偏移和弹窗自身尺寸计算精准位置,同时监听窗口resize事件动态更新位置。

React组件代码

import { useState, useRef, useEffect } from 'react';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';

const Calendar = () => {
  const [activeEvent, setActiveEvent] = useState(null);
  const [popupPos, setPopupPos] = useState({ top: 0, left: 0 });
  const popupRef = useRef(null);

  const handleEventClick = (info) => {
    const eventEl = info.el;
    const rect = eventEl.getBoundingClientRect();
    // 计算弹窗位置:事件正上方,水平居中,预留8px间距
    const popupHeight = popupRef.current?.offsetHeight || 0;
    const popupWidth = popupRef.current?.offsetWidth || 0;
    setPopupPos({
      top: rect.top + window.scrollY - popupHeight - 8,
      left: rect.left + window.scrollX + (rect.width / 2) - (popupWidth / 2)
    });
    setActiveEvent(info.event);
  };

  // 窗口resize时重新计算弹窗位置
  useEffect(() => {
    if (!activeEvent) return;
    const updatePos = () => {
      const eventEl = document.querySelector(`[data-event-id="${activeEvent.id}"]`);
      if (!eventEl) return;
      const rect = eventEl.getBoundingClientRect();
      const popupHeight = popupRef.current?.offsetHeight || 0;
      const popupWidth = popupRef.current?.offsetWidth || 0;
      setPopupPos({
        top: rect.top + window.scrollY - popupHeight - 8,
        left: rect.left + window.scrollX + (rect.width / 2) - (popupWidth / 2)
      });
    };
    window.addEventListener('resize', updatePos);
    return () => window.removeEventListener('resize', updatePos);
  }, [activeEvent]);

  // 点击外部关闭弹窗
  useEffect(() => {
    const closePopup = (e) => {
      if (popupRef.current && !popupRef.current.contains(e.target) && !e.target.closest('.fc-event')) {
        setActiveEvent(null);
      }
    };
    document.addEventListener('click', closePopup);
    return () => document.removeEventListener('click', closePopup);
  }, []);

  return (
    <div style={{ position: 'relative', minHeight: '600px' }}>
      <FullCalendar
        plugins={[dayGridPlugin]}
        events={[
          { id: '1', title: '黄色事件', start: '2024-05-20', backgroundColor: 'yellow' }
        ]}
        eventClick={handleEventClick}
        eventContent={({ event }) => (
          <div data-event-id={event.id} className="custom-yellow-event">{event.title}</div>
        )}
      />
      {activeEvent && (
        <div
          ref={popupRef}
          style={{
            position: 'absolute',
            top: popupPos.top,
            left: popupPos.left,
            backgroundColor: '#fff',
            padding: '10px 16px',
            border: '1px solid #e0e0e0',
            borderRadius: '6px',
            zIndex: 9999,
            boxShadow: '0 2px 12px rgba(0,0,0,0.1)'
          }}
        >
          <h5 style={{ margin: 0, color: '#333' }}>{activeEvent.title}</h5>
          <p style={{ margin: '6px 0 0', color: '#666', fontSize: '14px' }}>
            开始时间:{activeEvent.start.toLocaleDateString()}
          </p>
        </div>
      )}
    </div>
  );
};

export default Calendar;

配套CSS

.custom-yellow-event {
  background-color: yellow !important;
  color: #000 !important;
  border: none !important;
  text-align: center;
}

方案二:利用FullCalendar单元格的相对定位(更贴合日历布局)

把弹窗通过React Portal挂载到事件所在的日历单元格内,利用单元格的相对定位实现弹窗的精准对齐,无需全局坐标计算。

核心代码片段

import ReactDOM from 'react-dom';

// 事件点击回调
const handleEventClick = (info) => {
  const cellContainer = info.el.closest('.fc-daygrid-day-frame');
  // 给单元格设置相对定位基准
  cellContainer.style.position = 'relative';
  setActiveEvent({
    event: info.event,
    mountContainer: cellContainer
  });
};

// 弹窗部分使用Portal挂载到单元格内
{activeEvent && ReactDOM.createPortal(
  <div
    style={{
      position: 'absolute',
      bottom: '100%', // 单元格底部对齐,即事件正上方
      left: '50%',
      transform: 'translateX(-50%)',
      marginBottom: '8px',
      backgroundColor: '#fff',
      padding: '10px 16px',
      border: '1px solid #e0e0e0',
      borderRadius: '6px',
      zIndex: 9999,
      boxShadow: '0 2px 12px rgba(0,0,0,0.1)'
    }}
    onClick={(e) => e.stopPropagation()}
  >
    <h5 style={{ margin: 0, color: '#333' }}>{activeEvent.event.title}</h5>
    <p style={{ margin: '6px 0 0', color: '#666', fontSize: '14px' }}>
      开始时间:{activeEvent.event.start.toLocaleDateString()}
    </p>
  </div>,
  activeEvent.mountContainer
)}

关键注意事项

  • 必须加上window.scrollY和window.scrollX,否则页面滚动后弹窗会偏移
  • 计算水平居中时要考虑弹窗自身的宽度,用(事件宽度/2) - (弹窗宽度/2)实现精准居中
  • 监听窗口resize事件,确保窗口尺寸变化后弹窗位置自动更新
  • 设置足够高的z-index避免弹窗被日历元素遮挡
  • 添加点击外部关闭弹窗的逻辑,提升用户体验

内容的提问来源于stack exchange,提问作者zichyboy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 16:43:10