Shopify Draggable + Vue 3 完整指南:打造现代化拖拽交互体验

简介

Draggable 是一个现代化的 JavaScript 拖拽库,由 Shopify 开发,提供了强大而灵活的拖拽功能。它不仅支持基本的拖拽操作,还提供了丰富的插件系统、事件处理和自定义选项,可以轻松实现复杂的拖拽交互。

主要特性

  • 🎯 模块化设计: 基于插件架构,按需加载功能
  • 📱 触摸友好: 完美支持移动设备和触摸操作
  • 🎨 高度可定制: 丰富的配置选项和样式控制
  • 🔧 插件系统: 内置多种插件扩展功能
  • 🌐 现代浏览器: 支持所有现代浏览器
  • 可访问性: 内置键盘导航和屏幕阅读器支持
  • 🚀 性能优化: 高效的事件处理和 DOM 操作

安装

复制代码
# npm
npm install @shopify/draggable

# yarn
yarn add @shopify/draggable

# pnpm
pnpm add @shopify/draggable

Vue 3.js 基础使用示例

基本拖拽功能

复制代码
<template>
  <div
    class="min-h-screen bg-gradient-to-br from-emerald-50 to-teal-100 py-12 px-4"
  >
    <div class="max-w-md mx-auto">
      <h3 class="text-2xl font-bold text-gray-800 text-center mb-8">
        🎯 基本拖拽排序
      </h3>
      <div ref="dragContainer" class="space-y-4">
        <div
          v-for="item in dragItems"
          :key="item.id"
          :data-id="item.id"
          class="draggable-item bg-white rounded-lg shadow-md hover:shadow-lg transition-all duration-200 p-6 cursor-move border border-gray-200 hover:border-emerald-300"
        >
          <div class="flex items-center">
            <svg
              class="w-5 h-5 text-gray-400 mr-3"
              fill="currentColor"
              viewBox="0 0 20 20"
            >
              <path
                d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"
              ></path>
            </svg>
            <div>
              <h4 class="font-semibold text-gray-800">{{ item.title }}</h4>
              <p class="text-sm text-gray-500">{{ item.description }}</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from "vue";
import { Sortable } from "@shopify/draggable";

const dragContainer = ref(null);
const dragItems = ref([
  { id: 1, title: "任务 1", description: "完成项目设计" },
  { id: 2, title: "任务 2", description: "开发核心功能" },
  { id: 3, title: "任务 3", description: "编写测试用例" },
  { id: 4, title: "任务 4", description: "部署到生产环境" },
]);

let sortableInstance = null;

onMounted(() => {
  nextTick(() => {
    sortableInstance = new Sortable(dragContainer.value, {
      draggable: ".draggable-item",
      mirror: {
        appendTo: "body",
        constrainDimensions: true,
      },
    });

    // 监听拖拽排序事件
    sortableInstance.on("sortable:sorted", (evt) => {
      const { oldIndex, newIndex } = evt;
      // 更新数据顺序
      const item = dragItems.value.splice(oldIndex, 1)[0];
      dragItems.value.splice(newIndex, 0, item);
    });

    // 监听拖拽开始事件
    sortableInstance.on("drag:start", (evt) => {
      console.log("开始拖拽:", evt.source.dataset.id);
    });

    // 监听拖拽结束事件
    sortableInstance.on("drag:stop", (evt) => {
      console.log("拖拽结束:", evt.source.dataset.id);
    });
  });
});
</script>

<style scoped>
/* Draggable 镜像样式 */
.draggable-mirror {
  opacity: 0.8;
  transform: rotate(2deg);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  border-radius: 0.5rem;
}

/* 拖拽中的原始元素样式 */
.draggable--is-dragging {
  opacity: 0.3;
  transform: scale(0.95);
}

/* 拖拽悬停目标样式 */
.draggable-container--over {
  background-color: #f0fdf4;
}

/* 拖拽项目悬停样式 */
.draggable-item:hover {
  transform: translateY(-2px);
}
</style>

多容器拖拽交换

复制代码
<template>
  <div
    class="min-h-screen bg-gradient-to-br from-purple-50 to-pink-50 py-12 px-4"
  >
    <div class="max-w-6xl mx-auto">
      <h3 class="text-3xl font-bold text-gray-800 text-center mb-8">
        🔄 多容器拖拽交换
      </h3>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
        <!-- 源容器 -->
        <div class="bg-white rounded-xl shadow-lg p-6">
          <h4 class="text-xl font-semibold text-gray-700 mb-4 text-center">
            📦 可用组件
          </h4>
          <div
            ref="sourceContainer"
            class="space-y-3 min-h-[400px] p-4 border-2 border-dashed border-gray-200 rounded-lg"
          >
            <div
              v-for="item in sourceItems"
              :key="item.id"
              :data-id="item.id"
              class="swappable-item bg-gradient-to-r from-purple-100 to-pink-100 border border-purple-200 rounded-lg p-4 cursor-move hover:shadow-md transition-all duration-200"
            >
              <div class="flex items-center">
                <div class="w-3 h-3 bg-purple-500 rounded-full mr-3"></div>
                <div>
                  <h5 class="font-medium text-gray-800">{{ item.name }}</h5>
                  <p class="text-sm text-gray-500">{{ item.type }}</p>
                </div>
              </div>
            </div>
          </div>
        </div>

        <!-- 目标容器 -->
        <div class="bg-white rounded-xl shadow-lg p-6">
          <h4 class="text-xl font-semibold text-gray-700 mb-4 text-center">
            🎯 设计画布
          </h4>
          <div
            ref="targetContainer"
            class="space-y-3 min-h-[400px] p-4 border-2 border-dashed border-blue-200 rounded-lg bg-blue-50"
          >
            <div
              v-for="item in targetItems"
              :key="item.id"
              :data-id="item.id"
              class="swappable-item bg-gradient-to-r from-blue-100 to-indigo-100 border border-blue-200 rounded-lg p-4 cursor-move hover:shadow-md transition-all duration-200"
            >
              <div class="flex items-center">
                <div class="w-3 h-3 bg-blue-500 rounded-full mr-3"></div>
                <div>
                  <h5 class="font-medium text-gray-800">{{ item.name }}</h5>
                  <p class="text-sm text-gray-500">{{ item.type }}</p>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, nextTick } from "vue";
import { Swappable } from "@shopify/draggable";

const sourceContainer = ref(null);
const targetContainer = ref(null);

const sourceItems = ref([
  { id: 1, name: "按钮组件", type: "UI组件" },
  { id: 2, name: "输入框", type: "UI组件" },
  { id: 3, name: "图片组件", type: "媒体组件" },
]);

const targetItems = ref([{ id: 4, name: "标题组件", type: "UI组件" }]);

let swappableInstance = null;

onMounted(() => {
  nextTick(() => {
    const containers = [sourceContainer.value, targetContainer.value];

    swappableInstance = new Swappable(containers, {
      draggable: ".swappable-item",
      mirror: {
        appendTo: "body",
        constrainDimensions: true,
      },
    });

    swappableInstance.on("swappable:stop", (evt) => {
      updateContainersData();
    });
  });
});

function updateContainersData() {
  // 更新源容器数据
  const sourceElements =
    sourceContainer.value.querySelectorAll(".swappable-item");
  const newSourceItems = Array.from(sourceElements)
    .map((el) => {
      const id = parseInt(el.dataset.id);
      return [...sourceItems.value, ...targetItems.value].find(
        (item) => item.id === id
      );
    })
    .filter(Boolean);

  // 更新目标容器数据
  const targetElements =
    targetContainer.value.querySelectorAll(".swappable-item");
  const newTargetItems = Array.from(targetElements)
    .map((el) => {
      const id = parseInt(el.dataset.id);
      return [...sourceItems.value, ...targetItems.value].find(
        (item) => item.id === id
      );
    })
    .filter(Boolean);

  sourceItems.value = newSourceItems;
  targetItems.value = newTargetItems;
}
</script>

<style scoped>
.draggable-mirror {
  opacity: 0.8;
  transform: rotate(3deg) scale(1.05);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

.swappable--is-dragging {
  opacity: 0.5;
}

.draggable-container--over {
  background-color: #f0f9ff;
  border-color: #0ea5e9;
}
</style>

核心类和插件

主要类

类名 描述 用途
Draggable 基础拖拽类 实现基本的拖拽功能
Sortable 可排序类 在容器内重新排序元素
Swappable 可交换类 在多个容器间交换元素
Droppable 可放置类 实现拖放到指定区域

常用插件

插件 描述 功能
Mirror 镜像插件 显示拖拽时的镜像元素
Focusable 焦点插件 键盘导航支持
Announcement 公告插件 屏幕阅读器支持
ScrollSensitive 滚动敏感插件 拖拽时自动滚动

常用配置选项

选项 类型 默认值 描述
draggable String .draggable-source--is-draggable 可拖拽元素选择器
handle String null 拖拽手柄选择器
delay Number 0 拖拽延迟时间(毫秒)
distance Number 0 触发拖拽的最小距离
mirror Object {} 镜像配置选项
scrollable Object {} 滚动配置选项

常用事件

事件 描述 触发时机
drag:start 开始拖拽 拖拽开始时
drag:move 拖拽移动 拖拽过程中
drag:stop 拖拽结束 拖拽结束时
sortable:start 开始排序 排序开始时
sortable:stop 排序结束 排序结束时
swappable:start 开始交换 交换开始时
swappable:stop 交换结束 交换结束时

最佳实践

  1. 性能优化: 使用事件委托,避免为每个元素绑定事件
  2. 数据同步: 及时更新 Vue 的响应式数据状态
  3. 用户体验: 提供清晰的视觉反馈和动画效果
  4. 可访问性: 启用键盘导航和屏幕阅读器支持
  5. 移动端适配: 测试触摸设备的拖拽体验
  6. 错误处理: 添加适当的错误处理和回滚机制

总结

Draggable 是一个功能强大且高度可定制的拖拽库,提供了完整的拖拽解决方案。通过其模块化的设计和丰富的插件系统,可以轻松实现各种复杂的拖拽交互需求。与 Vue 3 结合使用时,能够创建出流畅且用户友好的拖拽界面。

Shopify Draggable + Vue 3 完整指南:打造现代化拖拽交互体验 - 高质量源码分享平台-免费下载各类网站源码与模板及前沿技术分享