实用的 Vue.Draggable 拖拽插件 - zptime/blog GitHub Wiki

Vue.Draggable 是一款基于 Sortable.js 实现的 vue 拖拽插件。支持移动设备拖拽,可以在不同列表间拖拽,支持 vue 2 过渡动画兼容,总之是一款非常优秀的 vue 拖拽组件。

Vue.Draggable 拖拽组件的特点是:换位置时,相当于是从当前位置移除,插入到另一个地方,插入地方的数据都往后移动一位。

Github 地址: https://github.com/SortableJS/Vue.Draggable

中文文档:https://www.itxst.com/vue-draggable/tutorial.html

安装

npm i -S vuedraggable

使用

// 引入
import draggable from "vuedraggable";

// 组件注册
export default {
  components: {
    draggable,
  },
};

// 使用
<draggable></draggable>;

实战

图示展示

模拟数据

export const mockData = {
  fruitColumns: {
    columns: [
      { name: "苹果", key: "apple", checked: true },
      { name: "梨子", key: "pear", checked: true },
      { name: "桃子", key: "peach", checked: false },
      { name: "西瓜", key: "watermelon", checked: true },
      { name: "榴莲", key: "durian", checked: false },
      { name: "百香果", key: "passion fruit", checked: true },
      { name: "哈密瓜", key: "Cantaloupe", checked: true },
      { name: "香蕉", key: "banana", checked: true },
      { name: "菠萝", key: "Pineapple", checked: true },
    ],
  },
  foodColumns: {
    columns: [
      { name: "粥", key: "pineapple", checked: true },
      { name: "米饭", key: "rice", checked: true },
      { name: "饺子", key: "dumpling", checked: true },
      { name: "面条", key: "noodle", checked: false },
      { name: "豆浆", key: "soy milk", checked: true },
      { name: "面包", key: "bread", checked: false },
    ],
  },
};

模块展示

数据源是一个列表,同时展示的是多个拖拽组件。

  1. 通过group来区分组,可以在相同的组之间互相拖拽。本次实践的是不同组的拖拽组件,不能互相拖拽。
  2. :draggable=".card" 定义了哪些元素是可以被拖动的
<template>
  <div class="m-list">
    <div
      class="m-list-item"
      v-for="(item, index) in dragList"
      :key="item.type"
      :id="item.type"
    >
      <template v-if="item && item.data && item.data.length">
        <div class="m-list-item-header">
          <div class="title">{{ item.title }}</div>
        </div>
        <draggable
          v-model="item.data"
          class="m-list-item-content"
          ghostClass="card-ghost"
          chosenClass="card-chosen"
          dragClass="card-drag"
          draggable=".card"
          :group="item.type"
          animation="300"
        >
          <transition-group>
            <div
              v-for="data in item.data"
              :key="data.key"
              :class="[
                  'card',
                  {
                    'card-active': item.checkedKeys.includes(data.key),
                  },
                ]"
              @click.stop.prevent="handleDragClick(data, item, index)"
            >
              {{ data.name }}
            </div>
          </transition-group>
        </draggable>
      </template>
    </div>
  </div>
</template>

数据处理

  1. 将模拟数据转为拖拽组件需要的数据:queryColumns()
  2. 点击事件,进行选中和未选中切换,对应的状态也会更改,默认是白色,选中时淡蓝色:handleDragClick()
<script>
  import * as R from "ramda";
  import draggable from "vuedraggable";

  // 拖拽列表
  const dragList = [
    { type: "fruit", key: "fruitColumns", title: "水果", data: [] },
    { type: "food", key: "foodColumns", title: "食物", data: [] },
  ];

  export default {
    data() {
      return {
        dragList,
      };
    },
    components: {
      draggable,
    },
    mounted() {
      this.queryColumns();
    },
    methods: {
      queryColumns() {
        this.dragList = R.pipe(
          R.toPairs,
          R.map(([key, props]) => {
            let result = R.mergeDeepRight(
              R.find(R.propEq("key", key))(this.dragList),
              {
                key,
                ...props,
              }
            );
            // 对应填充
            result.data = R.pathOr([], ["columns"], result);
            result.checkedKeys = R.map(
              (o) => o.key,
              R.filter((r) => r.checked, R.pathOr([], ["data"], result))
            );
            return result;
          })
        )(mockData);
      },
      handleDragClick(data, item, index) {
        if (!R.includes(data.key, item.checkedKeys)) {
          item.checkedKeys = R.append(data.key, item.checkedKeys);
        } else {
          item.checkedKeys = R.filter((o) => o !== data.key, item.checkedKeys);
        }
        // 此处要注意一下,更改的方式
        R.adjust(index, () => changeCheckStatus(item), this.dragList);
      },
    },
  };
</script>

拖拽样式处理

  1. ghostClass="card-ghost":设置拖动元素的占位符样式,需要加!important 才能生效(粉色背景,放大)
  2. chosenClass="card-chosen":被选中目标的样式,需要加!important 才能生效(淡蓝色背景)
  3. dragClass="card-drag":拖动元素的样式,需要加!important 才能生效(蓝色背景)
<style lang="scss" scoped>
  .m-list {
    width: 500px;
    &-item {
      margin-bottom: 32px;
      cursor: move;
      &:last-of-type {
        margin-bottom: 16px;
      }
      &-header {
        display: flex;
        margin-bottom: 20px;
        align-items: center;
        justify-content: space-between;
        line-height: 20px;
        .title {
          font-size: 16px;
          font-weight: bold;
        }
      }
      &-content {
        display: flex;
        flex-wrap: wrap;
        > span {
          flex: 1;
          display: flex;
          flex-wrap: wrap;
        }
        /* 被拖拽对象的样式 */
        .card {
          display: flex;
          align-items: center;
          justify-content: center;
          width: 156px;
          height: 40px;
          border: 1px solid #ccc;
          border-radius: 4px;
          margin-right: 8px;
          margin-bottom: 8px;
          // 动画实现
          transition: transform 0.3s;

          &-active {
            color: #1890ff;
            border-color: #a8d2ee;
            background-color: #e6f7ff;
            cursor: move;
          }

          /* 被选中样式:必须在ghost样式之前 */
          &-chosen {
            color: #fff;
          }

          /* 拖动元素样式 */
          &-drag {
            background-color: #1890ff !important;
            color: #fff;
            border: none;
          }

          /* 拖动元素占位符样式 */
          &-ghost {
            background-color: pink !important;
            color: #fff;
            transform: scale(1.08);
          }
        }
        .card + .card {
          margin-bottom: 8px;
        }
      }
    }
  }
</style>

视频效果展示

图片效果展示

⚠️ **GitHub.com Fallback** ⚠️