🔥 手写 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. 自定义手柄样式(比如改成虚线、加图标)。

📝 总结

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

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

相关推荐
恋猫de小郭39 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端