ReactJS如何实现全设备支持鼠标与触摸的拖拽、放置及缩放功能?
Hey there! 看起来你已经在预约周历UI上搞定了跨设备的拖拽放置,现在要加缩放功能对吧?结合ReactDND和react-resizable确实需要注意一些细节,尤其是触摸适配和事件冲突的问题,我来帮你梳理下可行的方案:
解决方案步骤
1. 为react-resizable开启触摸支持
react-resizable默认对触摸操作的支持不够完善,需要手动配置来适配触摸设备。核心是给缩放手柄添加触摸事件处理,同时避免浏览器默认触摸行为干扰:
import { ResizableBox } from 'react-resizable'; import 'react-resizable/css/styles.css'; // 在你的预约块组件中使用ResizableBox <ResizableBox width={blockWidth} height={blockHeight} // 自定义缩放手柄,确保触摸区域足够大 handle={<div className="resize-handle" />} handleProps={{ onTouchStart: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), style: { width: '24px', height: '24px', background: '#4285F4', position: 'absolute', bottom: 4, right: 4, cursor: 'se-resize', touchAction: 'none', // 阻止浏览器默认触摸滚动/缩放 borderRadius: '4px', }, }} onResizeStop={(e, data) => { // 缩放结束后更新预约块尺寸 updateBlockSize(data.size.width, data.size.height); }} > {/* 已用ReactDND包装的拖拽组件 */} <DraggableAppointmentBlock /> </ResizableBox>
2. 解决ReactDND与react-resizable的事件冲突
两个库都会监听鼠标/触摸起始事件,容易互相干扰。我们需要明确区分:点击缩放手柄时触发缩放,点击其他区域触发拖拽:
import { useDrag } from 'react-dnd'; const [{ isDragging }, drag, preview] = useDrag({ type: 'APPOINTMENT_BLOCK', item: { id: blockId, ...blockData }, beginDrag: (monitor) => { const { x, y } = monitor.getClientOffset(); // 检查点击的是否是缩放手柄 const targetElement = document.elementFromPoint(x, y); if (targetElement?.classList.contains('resize-handle')) { return null; // 不触发拖拽 } return { id: blockId, ...blockData }; }, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), });
3. 结合周历网格的缩放对齐逻辑
因为你的周历按30分钟时段划分,缩放时需要让预约块自动对齐网格,不能出现非标准时长/跨天:
// 定义网格单位对应的像素值 const GRID_ROW_HEIGHT = 60; // 30分钟对应60px高度 const GRID_COL_WIDTH = 150; // 一天对应150px宽度 const handleResizeStop = (e, data) => { // 对齐高度到30分钟倍数 const alignedHeight = Math.round(data.size.height / GRID_ROW_HEIGHT) * GRID_ROW_HEIGHT; // 对齐宽度到整列数 const alignedWidth = Math.round(data.size.width / GRID_COL_WIDTH) * GRID_COL_WIDTH; // 计算对应的业务数据 const duration = (alignedHeight / GRID_ROW_HEIGHT) * 30; // 分钟数 const crossDays = alignedWidth / GRID_COL_WIDTH; // 跨天数 // 更新预约块数据 updateBlock(blockId, { width: alignedWidth, height: alignedHeight, duration, crossDays }); };
4. 跨设备优化细节
- 确保缩放手柄尺寸不小于20x20px,方便触摸操作
- 给拖拽/缩放状态添加视觉反馈(比如拖拽时半透明、缩放时手柄变色)
- 在触摸设备上测试,避免出现拖拽和缩放互相触发的情况
完整示例组件
import React from 'react'; import { useDrag } from 'react-dnd'; import { ResizableBox } from 'react-resizable'; import 'react-resizable/css/styles.css'; const GRID_ROW_HEIGHT = 60; const GRID_COL_WIDTH = 150; const DraggableResizableAppointment = ({ blockId, blockData, updateBlock }) => { const [{ isDragging }, drag, preview] = useDrag({ type: 'APPOINTMENT_BLOCK', item: { id: blockId, ...blockData }, beginDrag: (monitor) => { const { x, y } = monitor.getClientOffset(); const targetElement = document.elementFromPoint(x, y); if (targetElement?.classList.contains('resize-handle')) return null; return { id: blockId, ...blockData }; }, collect: (monitor) => ({ isDragging: monitor.isDragging() }), }); const handleResizeStop = (e, data) => { const alignedHeight = Math.round(data.size.height / GRID_ROW_HEIGHT) * GRID_ROW_HEIGHT; const alignedWidth = Math.round(data.size.width / GRID_COL_WIDTH) * GRID_COL_WIDTH; const duration = (alignedHeight / GRID_ROW_HEIGHT) * 30; const crossDays = alignedWidth / GRID_COL_WIDTH; updateBlock(blockId, { ...blockData, width: alignedWidth, height: alignedHeight, duration, crossDays, }); }; return ( <preview> <ResizableBox width={blockData.width} height={blockData.height} handle={<div className="resize-handle" />} handleProps={{ onMouseDown: (e) => e.stopPropagation(), onTouchStart: (e) => e.stopPropagation(), style: { width: '24px', height: '24px', background: '#4285F4', position: 'absolute', bottom: 4, right: 4, cursor: 'se-resize', touchAction: 'none', borderRadius: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontSize: '12px', }, }} onResizeStop={handleResizeStop} style={{ position: 'absolute', top: blockData.top, left: blockData.left, background: isDragging ? 'rgba(66, 133, 244, 0.8)' : '#4285F4', borderRadius: '4px', padding: '8px', color: 'white', cursor: 'move', zIndex: isDragging ? 1000 : 10, }} > <div ref={drag}> <div>{blockData.title}</div> <div style={{ fontSize: '12px', marginTop: '4px' }}>{`${blockData.duration}分钟`}</div> </div> </ResizableBox> </preview> ); }; export default DraggableResizableAppointment;
内容的提问来源于stack exchange,提问作者Mithun Ganatra
相关产品推荐
相关产品推荐

