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

基于React实现支持点击拖拽选择的日历组件

实现React周度日程拖拽选时段创建事件的思路

1. 先搭基础框架与定义核心数据结构

  • 构建周度日历网格布局:按星期几(列)+ 小时段(行)划分单元格,每个单元格对应具体星期几和小时。
  • 定义关键状态:
    • selectedRange:存储拖拽选中的时间段,结构为{ start: Date, end: Date },记录拖拽起止时间。
    • events:数组,存储已创建的事件,每个事件包含id、start、end、title等字段。

2. 处理拖拽的三个核心事件

拖拽选时段依赖三个鼠标事件,拆分逻辑实现:

  • mousedown:在每个时段单元格上绑定该事件,记录当前单元格对应的时间作为拖拽起点,初始化selectedRange。
  • mousemove:全局监听该事件(避免拖拽出网格时失效),实时计算鼠标所在单元格的时间,更新selectedRange的end值,同时校验边界——确保结束时间不早于开始时间。
  • mouseup:全局监听,鼠标松开时判断selectedRange是否有效(时长大于0),若有效则触发事件创建流程(如弹输入框),最后清空selectedRange。

3. 给选中范围加视觉反馈

  • 简单方案:根据selectedRange的时间范围,给对应单元格添加高亮CSS类(比如改变背景色)。
  • 流畅方案:用绝对定位的div做覆盖层,根据起止时间计算它的位置、宽度和高度,拖拽时实时更新样式,视觉更连贯。

4. 把DOM位置转换成时间

将鼠标坐标转为具体时间的核心逻辑:

  • 列对应星期几:通过clientX计算所在列,映射到具体星期日期(比如第0列对应当前周周日)。
  • 行对应小时:通过clientY计算所在行,映射到具体小时数(比如第8行对应8点)。
  • 统一用Date对象存储起止时间,方便后续事件存储与渲染。

5. 事件创建的收尾逻辑

鼠标松开后,若选中时间段有效:

  • 弹出模态框,让用户输入事件标题、描述等信息。
  • 用户确认后,将新事件对象添加到events数组,完成创建。

6. 细节优化

  • 给mousemove加防抖,避免频繁更新状态导致页面卡顿。
  • 兼容移动端:将鼠标事件替换为触摸事件(touchstart、touchmove、touchend)。
  • 处理跨天边界:若业务不允许跨天,限制end时间不超过当天24点。

核心代码片段示例

import { useState, useEffect } from 'react';

const WeeklyCalendar = () => {
  const [selectedRange, setSelectedRange] = useState(null);
  const [events, setEvents] = useState([]);

  // 处理鼠标按下,记录拖拽起点
  const handleCellMouseDown = (dayOffset, hour) => {
    const today = new Date();
    // dayOffset: 0=周日,1=周一...6=周六,计算选中的日期
    const startDate = new Date(today.setDate(today.getDate() + (dayOffset - today.getDay())));
    startDate.setHours(hour, 0, 0, 0);
    setSelectedRange({ start: startDate, end: new Date(startDate) });
  };

  useEffect(() => {
    const handleMouseMove = (e) => {
      if (!selectedRange) return;

      // 获取日历容器相对坐标
      const calendarEl = document.querySelector('.weekly-calendar');
      const rect = calendarEl.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;

      // 假设每列宽度100px,每行高度50px,计算对应星期和小时
      const dayOffset = Math.floor(x / 100);
      const hour = Math.floor(y / 50);

      // 计算结束时间
      const endDate = new Date(selectedRange.start);
      endDate.setDate(endDate.getDate() + (dayOffset - selectedRange.start.getDay()));
      endDate.setHours(hour, 0, 0, 0);

      // 确保结束时间不早于开始时间
      if (endDate < selectedRange.start) return;

      setSelectedRange(prev => ({ ...prev, end: endDate }));
    };

    const handleMouseUp = () => {
      if (!selectedRange || selectedRange.end.getTime() === selectedRange.start.getTime()) {
        setSelectedRange(null);
        return;
      }

      // 弹出输入框(实际项目用模态框)
      const title = prompt('请输入事件标题');
      if (title) {
        const newEvent = {
          id: Date.now(),
          start: selectedRange.start,
          end: selectedRange.end,
          title
        };
        setEvents(prev => [...prev, newEvent]);
      }
      setSelectedRange(null);
    };

    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, [selectedRange]);

  // 渲染日历网格(简化示例)
  return (
    <div className="weekly-calendar" style={{ display: 'flex', border: '1px solid #eee' }}>
      {[0,1,2,3,4,5,6].map(dayOffset => (
        <div key={dayOffset} style={{ width: '100px', borderRight: '1px solid #eee' }}>
          {Array.from({length:24}).map((_, hour) => (
            <div
              key={hour}
              style={{ height: '50px', borderBottom: '1px solid #eee' }}
              onMouseDown={() => handleCellMouseDown(dayOffset, hour)}
              className={selectedRange && 
                hour >= selectedRange.start.getHours() && 
                hour <= selectedRange.end.getHours() &&
                dayOffset === selectedRange.start.getDay()
                ? 'bg-blue-100' : ''}
            />
          ))}
        </div>
      ))}
    </div>
  );
};

export default WeeklyCalendar;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 11:35:02