一、为什么要用VueDraggable?

如果你做过网页开发,肯定遇到过这样的需求:‌让用户能自己拖拽组件,拼出一个表单或者页面布局‌。比如常见的问卷调查系统、网站后台的页面编辑器等。

如果用原生JavaScript实现拖拽功能,需要处理大量事件监听、坐标计算,代码复杂容易出错。而 ‌VueDraggable‌ 是一个现成的Vue组件,它帮我们封装好了拖拽的核心逻辑,只需要简单配置就能实现以下功能:

  1. 拖拽排序:列表项自由调整顺序
  2. 跨容器拖拽‌:把元素从一个区域拖到另一个区域
  3. 自动同步数据:拖拽后的顺序自动更新到数据中

二、30分钟快速上手

2.1 安装组件

在项目中运行以下命令安装:

# npm管理器
npm install vuedraggable

# yarn管理器
yarn add vuedraggable

  注:不同版本可能有不同的回调方法及回调参数,如非最新版本,需参考官方文档使用,官方文档链接于本文文末。

2.2 基础示例:拖拽列表

<template>
  <!-- 一个可以拖拽的列表 -->
  <draggable 
    v-model="myList" 
    item-key="id"
  >
    <template #item="{element}">
      <div class="item">
        {{ element.name }}
      </div>
    </template>
  </draggable>
</template>

<script>
import draggable from 'vuedraggable'

export default {
  components: { draggable },
  data() {
    return {
      // 初始列表数据
      myList: [
        { id: 1, name: '苹果' },
        { id: 2, name: '香蕉' },
        { id: 3, name: '橘子' }
      ]
    }
  }
}
</script>

效果:列表中的水果可以自由拖拽排序,且 myList 数据会自动更新!


三、实战案例:拖拽生成表单

3.1 场景说明

我们要做一个表单设计器:左侧是组件列表(输入框、下拉框等),用户拖拽到右侧画布后,自动在数据库中创建对应字段,同时于右侧生成对应的组件

[左侧组件列表]          [右侧画布]
▼ 输入框               +-------------------+
▼ 下拉框               | 已拖入的组件会    |
▼ 单选按钮             | 自动出现在这里    |
                      | 并保存到数据库!  |
                      +-------------------+

3.2 实现步骤

3.2.1:准备组件库
<template>
  <!-- 左侧组件库 -->
  <div class="component-panel">
    <h3>表单组件库</h3>
    <draggable 
      :list="componentList" 
      :group="{ name: 'form', pull: 'clone' }"
      :sort="false"
    >
      <template #item="{ element }">
        <div class="component-item">
          <i class="icon">{{ element.icon }}</i>
          {{ element.label }}
        </div>
      </template>
    </draggable>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 预定义可拖拽的组件
      componentList: [
        { 
          type: 'input',
          label: '文本输入框',
          icon: '📝',
          fieldName: '' // 等待用户填写字段名
        },
        {
          type: 'select',
          label: '下拉选择框',
          icon: '🔽',
          options: ['选项1', '选项2']
        }
      ]
    }
  }
}
</script>

说明:group 的 pull: 'clone' 表示拖拽时复制组件,而不是移动

3.2.2:接收拖拽区域
<template>
  <!-- 右侧画布 -->
  <div class="canvas">
    <h3>表单设计区</h3>
    <draggable 
      v-model="droppedComponents" 
      group="form"
      @end="handleDrop"
    >
      <template #item="{ element }">
        <!-- 动态渲染不同组件 -->
        <div class="form-item">
          <label>{{ element.label }}</label>
          <input 
            v-if="element.type === 'input'"
            type="text" 
            :placeholder="element.placeholder"
          >
          <select v-else-if="element.type === 'select'">
            <option v-for="(opt, i) in element.options" :key="i">
              {{ opt }}
            </option>
          </select>
        </div>
      </template>
    </draggable>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 存放用户拖入的组件
      droppedComponents: []
    }
  },
  methods: {
    // 拖拽结束时触发
    async handleDrop(evt) {
      const newComponent = this.droppedComponents[evt.newIndex];
      
      // 调用接口保存到数据库
      const res = await axios.post('/api/save-field', {
        type: newComponent.type,
        fieldName: newComponent.fieldName,
        options: newComponent.options
      });
      
      // 给组件绑定数据库生成的ID
      newComponent.dbId = res.data.id;
    }
  }
}
</script>

3.3 常见问题

Q1:如何避免重复字段名?

在拖拽结束后检查字段名是否已存在:

handleDrop(evt) {
  const newField = this.droppedComponents[evt.newIndex];
  
  // 检查重复
  const isExist = this.droppedComponents.some(
    item => item.fieldName === newField.fieldName
  );
  
  if(isExist) {
    alert('字段名不能重复!');
    this.droppedComponents.splice(evt.newIndex, 1); // 移除重复项
    return;
  }
  
  // ...保存到数据库
}

Q2:如何修改组件属性?

添加双击编辑功能:

<template #item="{ element }">
  <div 
    class="form-item"
    @dblclick="showEditPanel(element)"
  >
    <!-- 组件内容 -->
  </div>
</template>

<script>
methods: {
  showEditPanel(component) {
    this.$prompt('请输入字段名', '编辑', {
      inputValue: component.fieldName
    }).then(name => {
      component.fieldName = name;
    });
  }
}
</script>

注:本文示例仅使用部分原生组件,如需使用自定义组件或更高级的组件,可于VueDraggable的item插槽中添加更多判断条件即可,使用方式类似。


四、进阶案例:栅格化布局

4.1 目标效果

用户可以通过拖拽调整布局区块的位置和大小,类似下图:

+-----------+-----------+
|   区块1   |   区块2   |
+-----------+-----------+
|        区块3         |
+----------------------+

4.2 实现代码


<draggable 
  v-model="gridLayout"
  class="grid-container"
  :options="{ animation: 300 }"
>
  <template #item="{element}">
    <div 
      class="grid-item"
      :style="{
        width: element.width + 'px',
        height: element.height + 'px'
      }"
    >
      {{ element.content }}
    </div>
  </template>
</draggable>

<script>
export default {
  data() {
    return {
      gridLayout: [
        { id: 1, width: 200, height: 100, content: '区块1' },
        { id: 2, width: 200, height: 100, content: '区块2' },
        { id: 3, width: 400, height: 200, content: '区块3' }
      ]
    }
  }
}
</script>

<style>
.grid-container {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.grid-item {
  border: 1px solid #ccc;
  padding: 20px;
  cursor: move;
}
</style>

五、常见问题解答

Q1:拖拽时样式错乱怎么办?

尝试添加以下CSS:

/* 拖拽时的预览效果 */
.sortable-chosen {
  opacity: 0.5;
}
/* 占位符样式 */
.sortable-ghost {
  background: #f0f0f0;
}

Q2:如何限制只能横向拖拽?

设置容器为横向排列:

.draggable-container {
  display: flex;
  overflow-x: auto;
}

Q3:移动端不生效怎么办?

在组件上添加 forceFallback: true 属性:

<draggable :options="{ forceFallback: true }">

VueDraggable常用属性如下:


六、小结

        本文内容均为本人闲时编写,内容不涉及公司内部项目代码,仅提供简单操作思路,无复杂实现流程,可为各位提供VueDraggable使用参考以及快速入门,更多功能可参阅官方文档,本文仅演示实现以上两个功能过程中用到的部分。

进一步学习‌:

        官方文档中给出了大量回调函数及组件参数,具体使用可参考:

vue.draggable中文文档 - itxst.com

如果有其他问题欢迎留言讨论,‌如果本文对你有帮助,请点赞收藏支持作者哦!‌ 😊

Logo

纵情码海钱塘涌,杭州开发者创新动! 属于杭州的开发者社区!致力于为杭州地区的开发者提供学习、合作和成长的机会;同时也为企业交流招聘提供舞台!

更多推荐