react+antd 可拖拽模态框组件
DraggableModal 可拖拽模态框组件使用说明
概述
DraggableModal
是一个基于 @dnd-kit/core
实现的可拖拽模态框组件,允许用户通过拖拽标题栏来移动模态框位置。该组件具有智能边界检测功能,确保模态框始终保持在可视区域内。
功能特性
- ✅ 可拖拽移动:支持通过鼠标拖拽移动模态框位置
- ✅ 智能边界检测:防止模态框被拖拽到屏幕可视区域外
- ✅ 响应式适配:根据窗口大小自动调整可拖拽范围
- ✅ 平滑交互:使用 CSS transform 实现流畅的拖拽动画
- ✅ 类型安全:完整的 TypeScript 类型支持
安装依赖
确保项目中已安装以下依赖:
npm install @dnd-kit/core @dnd-kit/utilities
# 或
yarn add @dnd-kit/core @dnd-kit/utilities
基本用法
1. 导入组件
import DraggableModal from './components/DraggableModal';
2. 基础示例
import React, { useState } from 'react';
import { Modal } from 'antd';
import DraggableModal from './components/DraggableModal';const ExampleComponent: React.FC = () => {const [visible, setVisible] = useState(false);return (<><button onClick={() => setVisible(true)}>打开可拖拽模态框</button><Modaltitle="可拖拽的模态框"open={visible}onCancel={() => setVisible(false)}modalRender={(modal) => (<DraggableModal>{modal}</DraggableModal>)}><p>这是一个可以拖拽的模态框内容</p></Modal></>);
};export default ExampleComponent;
3. 与 Antd Modal 结合使用
import React, { useState } from 'react';
import { Modal, Form, Input, Button } from 'antd';
import DraggableModal from '@/pages/StdFormEdit/components/DraggableModal';const FormModal: React.FC = () => {const [visible, setVisible] = useState(false);const [form] = Form.useForm();const handleSubmit = async () => {try {const values = await form.validateFields();console.log('表单数据:', values);setVisible(false);} catch (error) {console.error('表单验证失败:', error);}};return (<><Button type="primary" onClick={() => setVisible(true)}>打开表单模态框</Button><Modaltitle="用户信息编辑"open={visible}onCancel={() => setVisible(false)}footer={[<Button key="cancel" onClick={() => setVisible(false)}>取消</Button>,<Button key="submit" type="primary" onClick={handleSubmit}>确定</Button>]}modalRender={(modal) => (<DraggableModal>{modal}</DraggableModal>)}><table ...></Modal></>);
};export default FormModal;
API 说明
DraggableModal Props
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
children | ReactNode | - | 需要包装的模态框内容 |
组件内部实现细节
DraggableWrapper Props
属性 | 类型 | 说明 |
---|---|---|
top | number | 模态框垂直位置偏移量 |
left | number | 模态框水平位置偏移量 |
children | ReactNode | 子组件内容 |
modalRef | RefObject<HTMLDivElement> | 模态框DOM引用 |
技术实现
核心特性
- 拖拽识别:自动识别具有
modal-header
类名的元素作为拖拽手柄 - 边界限制:
- 垂直方向:上边界 -100px,下边界为窗口高度减去模态框高度再减去100px
- 水平方向:限制在窗口宽度范围内,保持模态框居中对称
- 位置计算:使用 CSS
transform
属性实现位置变换,性能优异
边界检测算法
// 垂直边界检测
const needRemoveMinHeight = -100; // 上边界
const needRemoveMaxHeight = winH - 100 - modalRef.current.clientHeight; // 下边界// 水平边界检测
const needRemoveWidth = (winW - modalRef.current.clientWidth) / 2; // 左右对称边界
使用注意事项
1. 模态框结构要求
确保被包装的模态框包含具有 modal-header
类名的标题栏元素:
// ✅ 正确 - Antd Modal 自动包含 modal-header 类名
<Modal title="标题">内容</Modal>// ❌ 错误 - 自定义模态框缺少 modal-header 类名
<div className="custom-modal"><div className="title">标题</div> {/* 缺少 modal-header 类名 */}<div>内容</div>
</div>
2. 性能优化建议
- 避免在
DraggableModal
内部频繁更新状态 - 对于复杂内容,建议使用
React.memo
优化子组件渲染
const OptimizedContent = React.memo(() => {return (<div>{/* 复杂内容 */}</div>);
});<DraggableModal><Modal title="优化示例"><OptimizedContent /></Modal>
</DraggableModal>
DraggableModal源码
import React, { useState, useRef, useLayoutEffect } from 'react';
import { DndContext, useDraggable } from '@dnd-kit/core';
import type { Coordinates } from '@dnd-kit/utilities';const DraggableWrapper = (props: any) => {const { top, left, children: node, modalRef } = props;const { attributes, isDragging, listeners, setNodeRef, transform } = useDraggable({id: 'draggable-title'});const dragChildren = React.Children.map(node.props.children, (child) => {if (!child) {return child;}if (child.type === 'div' && child.props?.className?.indexOf('modal-header') >= 0) {return React.cloneElement(child, {'data-cypress': 'draggable-handle',style: { cursor: 'move' },...listeners});}return child;});let offsetX = left;let offsetY = top;if (isDragging) {offsetX = left + (transform?.x ?? 0);offsetY = top + transform?.y;}return (<divref={(el) => {setNodeRef(el);if (modalRef) modalRef.current = el;}}{...attributes}style={{transform: `translate(${offsetX ?? 0}px, ${offsetY ?? 0}px)`} as React.CSSProperties}>{React.cloneElement(node, {}, dragChildren)}</div>);
};const DraggableModal = (props: any) => {const [{ x, y }, setCoordinates] = useState<Coordinates>({x: 0,y: 0});const modalRef = useRef<HTMLDivElement>(null);const [modalSize, setModalSize] = useState({ width: 0, height: 0 });useLayoutEffect(() => {if (modalRef.current) {const rect = modalRef.current.getBoundingClientRect();setModalSize({ width: rect.width, height: rect.height });}}, [props.children]);return (<DndContextonDragEnd={({ delta }) => {const winW = window.innerWidth;const winH = window.innerHeight;const needRemoveMinHeight = -100;const needRemoveWidth = (winW - modalRef.current.clientWidth) / 2;const needRemoveMaxHeight = winH - 100 - modalRef.current.clientHeight;const newX = x + delta.x;const newY = y + delta.y;let curNewY = newY;if (newY < 0) {curNewY = newY < needRemoveMinHeight ? needRemoveMinHeight : newY;} else {curNewY = newY > needRemoveMaxHeight ? needRemoveMaxHeight : newY;}if (Math.abs(newX) < needRemoveWidth) {setCoordinates({x: newX,y: curNewY});} else {setCoordinates({x: newX < 0 ? 0 - needRemoveWidth : needRemoveWidth,y: curNewY});}}}><DraggableWrapper top={y} left={x} modalRef={modalRef}>{props.children}</DraggableWrapper></DndContext>);
};export default DraggableModal;
3. 兼容性说明
- 支持现代浏览器(Chrome 88+、Firefox 84+、Safari 14+)
- 移动端暂不支持拖拽功能
- 需要 React 16.8+ 版本支持
故障排除
常见问题
Q: 模态框无法拖拽?
A: 检查以下几点:
- 确保模态框标题栏包含
modal-header
类名 - 确认
@dnd-kit/core
依赖已正确安装 - 检查是否有其他元素阻止了拖拽事件
Q: 拖拽时出现跳跃现象?
A: 这通常是由于 CSS 样式冲突导致,确保没有其他 transform
样式影响模态框定位。
Q: 模态框被拖拽到屏幕外?
A: 组件内置了边界检测,如果出现此问题,请检查窗口大小变化时是否正确触发了重新计算。
版本历史
- v1.0.0: 初始版本,支持基础拖拽功能和边界检测