基于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
相关产品推荐
相关产品推荐

