# 列表拖拽的Vue组件

效果如下

# 1、组件本身

ListDrag.vue


<script>
const itemHeight = 30;
export default {
  name: 'ListDrag',
  // mixins: [],
  // components: {},
  props: {
    // 自定义内容类型
    renderContent: {
      type: Function,
      default: (h) => {
        return h('span', '');
      }
    },
    value: {
      type: Array,
      default: () => []
    },
    listKey: String
  },
  //   emits: ['input'],
  data() {
    return {
      list: []
    };
  },

  // computed: {},
  // filters: {},
  watch: {
    value: {
      handler: function (newValue = []) {
        this.list = [...newValue];
      },
      deep: true
    }
    /* list: {
      handler: function (newList) {
        this.$emit(newList);
      },
      deep: true
    } */
  },
  created() {
    this.list = [...this.value];
    this.dragTarget = null;
    this.allItemElList = [];
    this.toIndex = -1;
  },
  // mounted() {},
  // beforeRouteEnter(to, from, next) { next(vm => {}) },
  // beforeRouteUpdate(to, from, next) {},
  // beforeRouteLeave(to, from, next) {},
  // beforeDestroy() {},
  // destroyed() {},
  methods: {
    onDragstartHandler(e) {
      //   e.preventDefault();
      console.debug('onDragstartHandler', e);
      const target = this.findDraggableListItem(e.target);
      if (target) {
        this.dragTarget = target;
      }
      this.allItemElList = this.$el.querySelectorAll('.drag-list-item');
    },
    onDragoverHandler(e) {
      e.preventDefault();
      const liList = this.allItemElList;
      for (let i = 0, len = liList.length; i < len; i++) {
        const liEl = liList[i];
        const { top, height } = liEl.getBoundingClientRect();
        const clientY = e.clientY;
        if (i === 0) {
          if (clientY <= top + height / 2) {
            // 说明需要插入到最上面
            this.toIndex = 0;
            this.setActiveElStyle(liEl);
            break;
          }
        }
        if (i === len - 1) {
          if (clientY > top + height / 2) {
            // 说明需要插入到底部
            // container.appendChild(dragTarget);
            this.toIndex = this.allItemElList.length;
            this.setActiveElStyle(liEl);
            // insertBeforeReferenceNode = null;
            break;
          }
        }

        const nextLiEl = liList[i + 1];
        if (liEl && nextLiEl) {
          const { top: nextTop, height: nextHeight } = nextLiEl.getBoundingClientRect();
          const middleLine = top + height / 2;
          const nextMiddleLine = nextTop + nextHeight / 2;
          if (middleLine < clientY && clientY <= nextMiddleLine) {
            // 说明在这两个元素中间
            this.toIndex = i + 1;
            this.setActiveElStyle(liEl);
          }
        }
      }
    },
    onDragendHandler(e) {
      e.preventDefault();
      console.debug('onDragendHandler', e);
      //   this.allItemElList = []
      if (this.dragTarget) {
        const fromIndex = this.dragTarget.dataset.index - 0;
        console.debug('fromIndex = ', fromIndex, 'toIndex = ', this.toIndex);

        if (fromIndex === this.toIndex) {
          // 不用移动
          this.toIndex = -1;
          this.resetActiveElStyle();
          return;
        }
        if (fromIndex < this.toIndex) {
          // 从前往后移  先插后删
          const fromData = this.list[fromIndex];
          const tempList = [...this.list];
          tempList.splice(this.toIndex, 0, fromData);
          tempList.splice(fromIndex, 1);
          this.list = tempList;
        }
        if (fromIndex > this.toIndex) {
          // 从后往前移 先删后插
          const fromData = this.list[fromIndex];
          const tempList = [...this.list];
          tempList.splice(fromIndex, 1);
          tempList.splice(this.toIndex, 0, fromData);
          this.list = tempList;
        }
        this.toIndex = -1;
        this.$emit('input', this.list);
        this.resetActiveElStyle();
      }
    },
    onDragenterHandler(e) {
      e.preventDefault();
      e.stopPropagation();
    },
    onDragleaveHandler(e) {
      e.preventDefault();
      e.stopPropagation();
    },

    onDropHandler(e) {
      //   e.preventDefault();
      console.debug('onDropHandler', e);
    },
    setActiveElStyle(el) {
      this.resetActiveElStyle();
      if (this.toIndex === 0) {
        el.style.marginTop = itemHeight + 'px';
      } else {
        el.style.marginBottom = itemHeight + 'px';
      }
    },
    resetActiveElStyle() {
      const liList = this.allItemElList;
      for (let i = 0, len = liList.length; i < len; i++) {
        const el = liList[i];
        el.style.marginBottom = '0';
        el.style.marginTop = '0';
      }
    },
    swapList(list, index1, index2) {
      const temp = list[index1];
      list[index1] = list[index2];
      list[index2] = temp;
      return list;
    },
    findDraggableListItem(
      el,
      predicate = (el) => {
        try {
          return el.getAttribute('draggable') === 'true';
        } catch (error) {
          return false;
        }
      }
    ) {
      if (!el) return null;
      let target = el;
      while (target) {
        if (predicate(target)) {
          break;
        } else {
          target = target.parentNode;
        }
      }
      return target;
    },
    // 向外提供接口 用于获取最新的列表顺序和内容
    getFinalList() {
      return this.list;
    },
    handleMoveUp(index) {
      return (event) => {
        event.stopPropagation();
        console.log(index, event.target);
        if (index === 0) {
          return;
        }
        this.list = this.swapList([...this.list], index, index - 1);
        console.log(this.list, '==');
        this.$emit('input', this.list);
      };
    },
    handleMoveDown(index) {
      return (event) => {
        console.log(index, event.target);
        event.stopPropagation();
        if (index === this.list.length - 1) {
          return;
        }
        this.list = this.swapList([...this.list], index, index + 1);
        console.log(this.list, '==');
        this.$emit('input', this.list);
      };
    },
    handleDelete(index) {
      return (event) => {
        event.stopPropagation();
        const deleteData = this.list.splice(index, 1)[0];
        this.$emit('input', this.list);
        this.$emit('delete', deleteData);
      };
    }
  },
  render(h) {
    return h(
      'div',
      {
        class: {
          'list-drag__wrapper': true
        },
        on: {
          dragstart: this.onDragstartHandler,
          dragend: this.onDragendHandler,
          dragover: this.onDragoverHandler,
          dragenter: this.onDragenterHandler,
          dragleave: this.onDragleaveHandler,
          drop: this.onDropHandler
        }
      },
      [
        h(
          'transition-group',
          {
            props: {
              name: 'xd-flip-list',
              tag: 'ul'
            }
          },
          this.list.map((item, index) => {
            const key = item[this.listKey];
            return h(
              'li',
              {
                key: key,
                class: {
                  'drag-list-item': true
                },
                attrs: {
                  draggable: true,
                  'data-index': index,
                  'data-key': key,
                  'data-identify': 'drag-item'
                }
              },
              [
                h(
                  'div',
                  {
                    class: { 'prefix-icon': true }
                  },
                  '⁝⁝'
                ),
                h(
                  'div',
                  {
                    class: { item__content: true }
                  },
                  [this.renderContent(h, item, index)]
                ),
                h(
                  'span',
                  {
                    class: { 'move-up__btn': true, 'move-btn--disabled': index === 0 },
                    props: { name: 'shangyi' },
                    on: {
                      click: this.handleMoveUp(index)
                    }
                  },
                  '上'
                ),
                h(
                  'span',
                  {
                    class: { 'move-down__btn': true, 'move-btn--disabled': index === this.list.length - 1 },
                    props: { name: 'xiayi' },
                    on: {
                      click: this.handleMoveDown(index)
                    }
                  },
                  '下'
                ),
                h(
                  'span',
                  {
                    class: { delete__btn: true },
                    props: { name: 'close' },
                    on: {
                      click: this.handleDelete(index)
                    }
                  },
                  '删'
                )
              ]
            );
          })
        )
      ]
    );
  }
};
</script>
<style lang="stylus" scoped>
$item-height = 30px;

.list-drag__wrapper {
  padding: 1px 0;

  .drag-list-item {
    background-color: #eee;
    height: $item-height;
    display: flex;
    box-sizing: border-box;
    align-items: center;
    padding: 0 4px;
    transition: margin 0.05s linear, transform 0.2s;
  }

  .item__content {
    flex: 1;
    overflow: hidden;
    height: 100%;
    display: flex;
    align-items: center;
  }

  .prefix-icon {
    width: 24px;
    font-size: 16px !important;
    text-align: center;
    cursor: move;
  }

  .move-up__btn, .move-down__btn, .delete__btn {
    font-size: 12px !important;
    margin-left: 6px;
  }

  .move-up__btn {
    cursor: n-resize;
  }

  .move-down__btn {
    cursor: s-resize;
  }

  .move-btn--disabled {
    color: #c3cbd6;
    cursor: not-allowed;
  }
}
</style>

# 2、组件用法

通过 renderContent 自定义需要渲染的内容

WARNING

这里其实就是模板语法的不足之后,有些需要通过slot自定义传入的内容,在这里就不适用, 尤其还需要v-for来循环生成动态内容时。 还在还有render可用。

listKey 是列表对象的唯一key值,用于v-for循环。

也支持v-model,前提是你的列表不需要额外特殊逻辑。

如果需要动态增加内容, 比如输入框, 下拉框等还需要修改列表的值时, input 事件回调就需要自己定义。如下面的逻辑

<template>
  <div style="max-width: 500px;">
    <ListDrag :value="list"
              :renderContent="renderContent"
              :listKey="__id__"
              @input="onInput"
              @delete="onDelete"></ListDrag>
    <button @click="handleAdd">add</button>
  </div>
</template>
<script>
// import { deepClone2 } from './utils/utils.js';
import ListDrag from './ListDrag.vue';
const __id__ = 'id_drag';
export default {
  name: 'Index',
  // mixins: [],
  components: {
    ListDrag
  },
  // props: {},
  data() {
    return {
      list: []
    };
  },
  // computed: {},
  // filters: {},
  // watch:{},
  created() {
    this.__id__ = __id__;
    this.__index__ = 0;
    this.listMap = new Map();
    this.getList();
  },
  // mounted() {},
  // beforeRouteEnter(to, from, next) { next(vm => {}) },
  // beforeRouteUpdate(to, from, next) {},
  // beforeRouteLeave(to, from, next) {},
  // beforeDestroy() {},
  // destroyed() {},
  methods: {
    getList() {
      const tempList = [];
      for (let i = 0, len = 8; i < len; i++) {
        const id = this.__index__.toString();
        const dataObj = { data: this.__index__, [__id__]: id };
        this.__index__++;
        tempList.push(dataObj);
        this.listMap.set(id, dataObj);
      }
      this.list = tempList;
    },
    renderContent(h, item, index) {
      // 可以根据唯一key值 渲染任何自己想要的东西
      /* if (item[__id__] == '3') {
        const label = this.list[index].name;
        return h('div', {}, [
          h('input', {
            attrs: {
              value: label,
              'data-name': label
            },
            on: {
              input: (e) => {
                console.log(e);
                // this.list[index].name = e.target.value;
                this.$set(this.list[index], 'name', e.target.value);
              }
            }
          }),
          h('span', {}, this.list[index].name)
        ]);
      } */
      return h('span', 'item-' + item.data);
    },
    onInput(newList = []) {
      // 组件只负责顺序的变更,其他自定的地方都自己写。 包括内容
      const tempList = [];
      for (let i = 0, len = newList.length; i < len; i++) {
        const item = newList[i];
        const id = item[__id__];
        tempList.push(this.listMap.get(id));
      }
      this.list = tempList;
      console.log(JSON.parse(JSON.stringify(this.list)));
    },
    onDelete(deleteData) {
      console.log('被删除的对象', deleteData);
    },
    handleAdd() {
      const id = this.__index__.toString();
      const dataObj = { data: this.__index__, [__id__]: id };
      this.__index__++;
      this.list.push(dataObj);
      this.listMap.set(id, dataObj);
    }
  }
};
</script>
<style lang="stylus" scoped></style>

上次更新: 1/22/2025, 9:39:13 AM