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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 10:40:04