DraggableModal 可拖拽模态框组件使用说明
概述
DraggableModal
是一个基于 @dnd-kit/core
实现的可拖拽模态框组件,允许用户通过拖拽标题栏来移动模态框位置。该组件具有智能边界检测功能,确保模态框始终保持在可视区域内。
功能特性
- ✅ 可拖拽移动:支持通过鼠标拖拽移动模态框位置
- ✅ 智能边界检测:防止模态框被拖拽到屏幕可视区域外
- ✅ 响应式适配:根据窗口大小自动调整可拖拽范围
- ✅ 平滑交互:使用 CSS transform 实现流畅的拖拽动画
- ✅ 类型安全:完整的 TypeScript 类型支持
安装依赖
确保项目中已安装以下依赖:
bash
npm install @dnd-kit/core @dnd-kit/utilities
# 或
yarn add @dnd-kit/core @dnd-kit/utilities
基本用法
1. 导入组件
typescript
import DraggableModal from './components/DraggableModal';
2. 基础示例
typescript
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>
<Modal
title="可拖拽的模态框"
open={visible}
onCancel={() => setVisible(false)}
modalRender={(modal) => (
<DraggableModal>
{modal}
</DraggableModal>
)}
>
<p>这是一个可以拖拽的模态框内容</p>
</Modal>
</>
);
};
export default ExampleComponent;
3. 与 Antd Modal 结合使用
typescript
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>
<Modal
title="用户信息编辑"
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
属性实现位置变换,性能优异
边界检测算法
typescript
// 垂直边界检测
const needRemoveMinHeight = -100; // 上边界
const needRemoveMaxHeight = winH - 100 - modalRef.current.clientHeight; // 下边界
// 水平边界检测
const needRemoveWidth = (winW - modalRef.current.clientWidth) / 2; // 左右对称边界
使用注意事项
1. 模态框结构要求
确保被包装的模态框包含具有 modal-header
类名的标题栏元素:
html
// ✅ 正确 - 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
优化子组件渲染
typescript
const OptimizedContent = React.memo(() => {
return (
<div>
{/* 复杂内容 */}
</div>
);
});
<DraggableModal>
<Modal title="优化示例">
<OptimizedContent />
</Modal>
</DraggableModal>
DraggableModal源码
typescript
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 (
<div
ref={(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 (
<DndContext
onDragEnd={({ 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: 初始版本,支持基础拖拽功能和边界检测