🔥 手写 Vue 自定义指令:实现内容区拖拽调整大小(超实用)

日常开发中经常遇到需要手动调整内容区大小的场景,比如侧边栏、弹窗、报表面板等。分享一个我写的「拖拽调整大小指令」,支持自定义最小尺寸、拖拽手柄样式,能监听尺寸变化

📌 先看效果

🛠 核心代码解析

指令文件 directives/resizable-full.js ,关键部分:

1. 指令钩子:初始化 + 更新 + 清理

Vue 指令的 3 个核心钩子,保证指令的生命周期完整:

js

scss 复制代码
export default {
  bind(el, binding) {
    // 指令绑定时初始化拖拽功能
    initResizable(el, binding);
  },
  update(el, binding) {
    // 禁用状态变化时,重新初始化
    if (binding.value?.disabled !== binding.oldValue?.disabled) {
      cleanupResizable(el); // 先清理旧的
      initResizable(el, binding); // 再初始化新的
    }
  },
  unbind(el) {
    // 指令解绑时,清理所有手柄和事件(避免内存泄漏)
    cleanupResizable(el);
  }
};

2. 初始化拖拽:创建手柄 + 核心逻辑

initResizable 是核心函数,主要做 2 件事:创建拖拽手柄、写拖拽逻辑。

(1)创建拖拽手柄

我只保留了「右下角」的拖拽手柄(其他方向注释掉了,需要的话自己解开),样式可自定义:

js

ini 复制代码
// 定义手柄配置(只留了bottom-right)
const handles = [
  { dir: 'bottom-right', style: { bottom: 0, right: 0, cursor: 'nwse-resize' } }
];

// 循环创建手柄元素
handles.forEach(handleConf => {
  const handle = document.createElement('div');
  handle.className = `resizable-handle resizable-handle--${handleConf.dir}`;
  handle.dataset.dir = handleConf.dir;
  
  // 手柄样式:小方块、半透明、hover高亮
  Object.assign(handle.style, {
    position: 'absolute',
    width: `${handleSize}px`,
    height: `${handleSize}px`,
    background: handleColor,
    opacity: '0.6',
    zIndex: 999,
    transition: 'opacity 0.2s',
    ...handleConf.style
  });

  // hover时手柄高亮
  handle.addEventListener('mouseenter', () => handle.style.opacity = '1');
  handle.addEventListener('mouseleave', () => handle.style.opacity = '0.6');

  el.appendChild(handle); // 把手柄加到目标元素上
  el._resizableConfig.handles.push(handle); // 存起来方便后续清理
});

(2)拖拽核心逻辑

分 3 步:按下鼠标(记录初始状态)→ 移动鼠标(计算新尺寸)→ 松开鼠标(触发回调 + 清理):

js

ini 复制代码
// 1. 按下鼠标:记录初始位置和尺寸
const mouseDownHandler = (e) => {
  const handle = e.target.closest('.resizable-handle');
  if (!handle) return;

  e.stopPropagation();
  e.preventDefault();
  
  const dir = handle.dataset.dir;
  const rect = el.getBoundingClientRect(); // 获取元素当前位置和尺寸

  // 存初始状态:鼠标位置、元素尺寸/位置
  startState = {
    dir,
    startX: e.clientX,
    startY: e.clientY,
    startWidth: rect.width,
    startHeight: rect.height
  };

  // 绑定移动/松开事件(绑在document上,避免拖拽时鼠标移出元素失效)
  document.addEventListener('mousemove', onMouseMove);
  document.addEventListener('mouseup', onMouseUp);
};

// 2. 移动鼠标:计算新宽高并赋值
const onMouseMove = (e) => {
  if (!startState) return;
  const { dir, startX, startY, startWidth, startHeight } = startState;
  let newWidth = startWidth;
  let newHeight = startHeight;

  // 只处理右下角拖拽:宽高都增加
  if (dir === 'bottom-right') {
    newWidth = startWidth + (e.clientX - startX);
    newHeight = startHeight + (e.clientY - startY);
  }

  // 限制最小宽高(避免拖到太小)
  newWidth = Math.max(minWidth, newWidth);
  newHeight = Math.max(minHeight, newHeight);

  // 给元素设置新尺寸
  el.style.width = `${newWidth}px`;
  el.style.height = `${newHeight}px`;
};

// 3. 松开鼠标:触发回调+清理事件
const onMouseUp = () => {
  // 拖拽结束,触发自定义回调,返回最新尺寸
  if (startState && el._resizableConfig.onResize) {
    el._resizableConfig.onResize({
      width: parseInt(el.style.width),
      height: parseInt(el.style.height)
    });
  }
  startState = null;
  // 移除事件(避免重复绑定)
  document.removeEventListener('mousemove', onMouseMove);
  document.removeEventListener('mouseup', onMouseUp);
};

// 给元素绑定按下事件
el.addEventListener('mousedown', mouseDownHandler);

3. 清理函数:避免内存泄漏

cleanupResizable 负责移除所有手柄元素和事件监听器,指令解绑时必执行:

js

javascript 复制代码
function cleanupResizable(el) {
  if (el._resizableConfig) {
    // 移除所有手柄
    el._resizableConfig.handles.forEach(handle => {
      if (handle.parentNode === el) el.removeChild(handle);
    });
    // 移除所有事件监听器
    el.removeEventListener('mousedown', el._resizableConfig.mouseDownHandler);
    document.removeEventListener('mousemove', el._resizableConfig.mouseMoveHandler);
    document.removeEventListener('mouseup', el._resizableConfig.mouseUpHandler);
    // 删除配置(释放内存)
    delete el._resizableConfig;
  }
}

🚀 如何使用?

  1. 全局注册指令(main.js):

js

javascript 复制代码
import resizableFull from './directives/resizable-full';
Vue.directive('resizable-full', resizableFull);
  1. 页面中使用

vue

xml 复制代码
<template>
  <!-- 给需要拖拽的元素加指令 -->
  <div 
    v-resizable-full="{
      minWidth: 300, // 最小宽度
      minHeight: 200, // 最小高度
      handleSize: 10, // 手柄大小
      handleColor: '#409eff', // 手柄颜色
      onResize: handleResize // 拖拽结束回调
    }"
    style="position: relative; width: 400px; height: 300px; border: 1px solid #eee;"
  >
    我是可拖拽调整大小的内容区~
  </div>
</template>

<script>
export default {
  methods: {
    // 拖拽结束,拿到最新尺寸
    handleResize({ width, height }) {
      console.log('新尺寸:', width, height);
    }
  }
};
</script>

💡 关键注意点(避坑)

  1. 目标元素必须设 position: relative/absolute/fixed:因为手柄是绝对定位,依赖父元素的定位;
  2. 事件绑在 document 上:拖拽时鼠标可能移出目标元素,绑在 document 上才不会断;
  3. 一定要清理事件 / 元素 :指令解绑时执行 cleanupResizable,避免内存泄漏;
  4. 最小尺寸限制 :通过 minWidth/minHeight 避免元素被拖到太小,影响体验。

🎨 扩展玩法

  1. 解开注释的其他 7 个方向手柄,实现全方向拖拽;
  2. 给手柄加 hover 提示(比如 "拖拽调整大小");
  3. 支持拖拽时实时触发回调(不止结束时);
  4. 自定义手柄样式(比如改成虚线、加图标)。

📝 总结

这个自定义指令核心是「创建拖拽手柄 + 监听鼠标事件 + 计算尺寸变化」,逻辑不复杂,可以根据自己的业务场景定制。亲测报表和弹窗都很适用~

如果觉得有用,可以点个赞收藏一下,下次需要直接翻出来用😜

相关推荐
ohyeah2 小时前
深入理解 React Hooks:useState 与 useEffect 的核心原理与最佳实践
前端·react.js
Cache技术分享2 小时前
275. Java Stream API - flatMap 操作:展开一对多的关系,拉平你的流!
前端·后端
apollo_qwe2 小时前
前端缓存深度解析:从基础到进阶的实现方式与实践指南
前端
周星星日记3 小时前
vue中hash模式和history模式的区别
前端·面试
Light603 小时前
Vue 高阶优化术:v-bind 与 v-on 的实战妙用与思维跃迁
前端·低代码·vue3·v-bind·组件封装·v-on·ai辅助开发
周星星日记3 小时前
5.为什么vue中使用query可以保留参数
前端·vue.js
lebornjose3 小时前
javascript - webgl中绑定(bind)缓冲区的逻辑是什么?
前端·webgl
瘦的可以下饭了3 小时前
Day05- CSS 标准流、浮动、Flex布局
前端
前端无涯3 小时前
React中setState后获取更新后值的完整解决方案
前端·react.js