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: 初始版本,支持基礎拖拽功能和邊界檢測