You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue项目使用Dragula拖拽时目标索引返回错误的问题

幻灯片侧边栏拖拽排序异常问题

我想给幻灯片侧边栏的卡片添加拖拽功能,用来调整幻灯片的位置,但目前拖拽时排序错误,原因是部分slide-card元素的data-index与DOM中的实际显示顺序不一致。


HTML代码

<div class="c-side-bar__opened__container__navbar" v-model="isSidebarOpen" ref="slideCards">
  <template v-for="(slide, index) in slides">
    <li
      class="c-side-bar__opened__container__navbar__slide"
      :class="{
        'c-side-bar__opened__container__navbar__slide--hover': index === activeSlideIndex,
      }"
      :data-index="index"
      :key="slide.ID"
    >
      <div
        class="c-side-bar__opened__container__navbar__slide__card"
        :class="{
          'c-side-bar__opened__container__navbar__slide__card--hover': index === activeSlideIndex,
        }"
      >
        <slide-card
          :slideID="slide.ID"
          :data-index="index"
          :index="index"
          :key="slide.ID"
          :totalCount="slides.length"
          :isSideBarComponent="isSideBarComponent"
        >
        </slide-card>
      </div>
      <span
        class="c-side-bar__opened__container__navbar__slide__index"
        :class="{'c-side-bar__opened__container__navbar__slide__index--hover': index === activeSlideIndex }"
        >{{ index + 1 }}</span
      >
    </li>
  </template>
  <li class="c-side-bar__opened__container__navbar__slide c-side-bar--add-slide">
    <app-button type="icon" theme="secondary" size="large" @click="addSlide">
      <app-icon name="plus"></app-icon>
    </app-button>
  </li>
</div>

TS代码

this.dragula = dragula([this.$refs.slideCards as Element], {
  direction: 'horizantal',
  isContainer: (el: Element) => {
    if (!this.draggedSlide) {
      return false;
    }
    if (el.classList.contains('c-side-bar__opened__container__navbar__slide__card')) {
      const targetIndex = parseInt(el.getAttribute('data-index'), 10);
      // 错误点:HTML元素的data-index有时和DOM显示顺序不一致
      this.draggedSlide.targetIndex = targetIndex;
    }
    return false;
  },
})
.on('drag', (draggingSlideCard: HTMLElement) => {
  this.isDragging = true;

  this.draggedSlide = {
    sourceIndex: parseInt(draggingSlideCard.getAttribute('data-index'), 10),
    targetIndex: parseInt(draggingSlideCard.getAttribute('data-index'), 10),
  };
})
.on('drop', () => {
  if (!this.draggedSlide) {
    return;
  }

  // 保存顺序变更后的新幻灯片列表
  this.moveSlide(this.draggedSlide.sourceIndex, this.draggedSlide.targetIndex);
  this.draggedSlide = null;
  this.isDragging = false;
});

问题根源与修复方案

问题根源

  1. Vue列表复用导致data-index过期:循环使用slide.ID作为:key,当slides数组顺序改变时,Vue会复用已有DOM元素,不会重新渲染,导致data-index仍保留旧的索引值,和DOM实际顺序不匹配。
  2. 依赖data-index获取目标索引不可靠:Dragula逻辑中通过data-index获取目标位置,而该值已过期,自然导致排序错误。
  3. 拼写错误:direction: 'horizantal'应为'horizontal',可能影响拖拽方向判断。

修复步骤

1. 修改列表的key值,确保DOM随数组顺序更新

将循环的:key从slide.ID改为index(或结合slide.ID与index),这样数组顺序变化时,Vue会重新渲染列表,同步更新data-index:

<li
  class="c-side-bar__opened__container__navbar__slide"
  :class="{
    'c-side-bar__opened__container__navbar__slide--hover': index === activeSlideIndex,
  }"
  :data-index="index"
  :key="index" <!-- 此处修改 -->
>
<!-- slide-card的key也同步修改 -->
<slide-card
  :slideID="slide.ID"
  :data-index="index"
  :index="index"
  :key="index" <!-- 或使用`${slide.ID}-${index}`确保唯一 -->
  :totalCount="slides.length"
  :isSideBarComponent="isSideBarComponent"
>
</slide-card>

2. 直接通过DOM位置计算索引,不依赖data-index

修改Dragula逻辑,通过元素在父容器中的位置获取真实索引:

this.dragula = dragula([this.$refs.slideCards as Element], {
  direction: 'horizontal', // 修正拼写错误
  moves: (el, source, handle) => {
    // 指定只有卡片元素可拖拽
    return el.classList.contains('c-side-bar__opened__container__navbar__slide__card');
  }
})
.on('drag', (draggingSlideCard: HTMLElement) => {
  this.isDragging = true;
  // 获取拖拽卡片的父li元素
  const parentLi = draggingSlideCard.closest('.c-side-bar__opened__container__navbar__slide');
  // 获取所有非添加按钮的slide项
  const slideItems = Array.from((this.$refs.slideCards as Element).querySelectorAll('.c-side-bar__opened__container__navbar__slide:not(.c-side-bar--add-slide)'));
  this.draggedSlide = {
    sourceIndex: slideItems.indexOf(parentLi),
    targetIndex: slideItems.indexOf(parentLi),
  };
})
.on('drop', (el, target, source, sibling) => {
  if (!this.draggedSlide) return;
  // 获取所有slide项
  const slideItems = Array.from((this.$refs.slideCards as Element).querySelectorAll('.c-side-bar__opened__container__navbar__slide:not(.c-side-bar--add-slide)'));
  let targetIndex;
  // 根据sibling元素计算目标位置
  if (sibling) {
    const siblingLi = sibling.closest('.c-side-bar__opened__container__navbar__slide');
    targetIndex = slideItems.indexOf(siblingLi);
  } else {
    // 没有sibling说明拖到了最后
    targetIndex = slideItems.length - 1;
  }
  // 执行幻灯片移动逻辑
  this.moveSlide(this.draggedSlide.sourceIndex, targetIndex);
  this.draggedSlide = null;
  this.isDragging = false;
});

3. 验证moveSlide逻辑

确保moveSlide函数能正确修改slides数组的顺序,示例如下:

moveSlide(fromIndex: number, toIndex: number) {
  const [movedSlide] = this.slides.splice(fromIndex, 1);
  this.slides.splice(toIndex, 0, movedSlide);
  // 可选:更新当前激活的幻灯片索引
  this.activeSlideIndex = toIndex;
}

内容的提问来源于stack exchange,提问作者Ramazan Arslan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 22:09:20