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

Vue3列表元素编辑相互影响问题求助(用vue-draggable-next等组件)

问题解决:实现拖拽元素的独立编辑功能

问题根源

  1. 克隆元素引用重复:拖拽克隆时直接复用原元素对象,导致多个元素共享同一引用,修改一个会影响所有同来源克隆元素。
  2. 删除按钮未传索引:deleteElement调用时未传递当前元素的index,无法精准删除目标元素。
  3. 编辑未同步当前值:点击编辑按钮后,输入框未填充当前元素的现有内容,用户体验差。
  4. 响应式变量错误嵌套:state和edit中嵌套使用ref,不符合Vue3响应式规范,可能导致数据更新异常。

修改方案

1. 修复拖拽克隆逻辑,生成独立元素

在拖拽结束事件中对克隆元素做深拷贝,并生成新的唯一ID,确保每个元素都是独立对象:

function onDragEnd(event) {
  if (event.added) {
    const newElement = JSON.parse(JSON.stringify(event.added.element));
    newElement.id = crypto.randomUUID();
    state.contentElements.splice(event.newIndex, 1, newElement);
    generateJSON();
  }
}

同时更新左侧拖拽组件的@end事件绑定:

<draggable
  class="row mt-2"
  :list="draggableElements"
  :sort="false"
  :group="{ name: 'cloneableItems', pull: 'clone', put: false }"
  @end="onDragEnd"
>

2. 修复删除按钮的索引传递

修改删除按钮的点击事件,传入当前元素的index:

<i class="bx bxs-trash-alt edit-icon" @click="deleteElement(index)"></i>

3. 编辑时同步当前元素值到输入框

更新editElement函数,将当前元素的文本内容填充到编辑输入框:

function editElement(index) {
  state.editedIndex = index;
  const element = state.contentElements[index];
  if (element.type === 'description') {
    edit.descriptionNewText = element.descriptionText;
  }
}

4. 修正响应式变量定义

移除state和edit中嵌套的ref,直接使用reactive定义:

const state = reactive({
  editedIndex: -1,
  contentElements: [],
  showJSONPanel: false,
  generatedJSON: "",
});

const edit = reactive({
  descriptionNewText: "",
});

完整修改后代码

<template>
  <div class="container-fluid row mt-2">
    <div class="col-md-3 float-start">
      <div class="panel-default border" style="background-color: #f7f9fa">
        <div class="panel-heading text-center">
          <h3 class="panel-title visible-panel-text">Draggable elements</h3>
        </div>

        <!-- Elements zone -->
        <draggable
          class="row mt-2"
          :list="draggableElements"
          :sort="false"
          :group="{ name: 'cloneableItems', pull: 'clone', put: false }"
          @end="onDragEnd"
        >
          <div
            class="col-sm-6 mb-2"
            v-for="element in draggableElements"
            :key="element.id"
          >
            <div class="border drag-element">{{ element.visibleName }}</div>
          </div>
        </draggable>

        <!-- Copy JSON panel -->
        <div
          class="panel-json json-container mt-2 border"
          style="background-color: #f7f9fa"
        >
          <div
            class="panel-heading text-center"
            @click="state.showJSONPanel = !state.showJSONPanel"
          >
            <h3
              class="panel-title"
              style="text-align: center; cursor: pointer; user-select: none"
            >
              JSON
            </h3>
          </div>
          <div class="panel-body" v-show="state.showJSONPanel">
            <textarea
              class="json-textarea form-control"
              v-model="state.generatedJSON"
              style="width: 90%; margin: 0 auto; resize: none"
            ></textarea>
            <button class="btn btn-secondary btn-sm" @click="copyJSON">
              Copy JSON
            </button>
          </div>
        </div>
      </div>
    </div>
    <div class="col-md-6">
      <!-- Show content -->
      <draggable
        class="w-100 h-100 border"
        :empty-insert-threshold="1000"
        :list="state.contentElements"
        @update="generateJSON"
        :group="{ name: 'putItems', pull: 'clone', put: true }"
      >
        <div
          v-for="(element, index) in state.contentElements"
          :key="element.id"
          class="container"
        >
          <template v-if="element.type == 'description'">
            <label>This is the description element</label>
            <div>{{ element.descriptionText }}</div>
          </template>

          <!-- Edit & delete -->
          <div
            class="iterable-icons-container"
            style="display: flex; justify-content: flex-end"
          >
            <!-- Edit icon -->
            <i class="bx bxs-cog edit-icon" @click="editElement(index)"></i>
            <!-- Delete icon -->
            <i class="bx bxs-trash-alt edit-icon" @click="deleteElement(index)"></i>
          </div>
        </div>
      </draggable>
    </div>

    <!-- Changes panel -->
    <div class="col-md-3 border">
      <div>
        <!-- Description -->
        <template
          v-if="
            state.editedIndex >= 0 &&
            state.contentElements[state.editedIndex].type == 'description'
          "
        >
          <label><strong>Description element</strong></label>
          <input type="text" v-model="edit.descriptionNewText" />

          <button class="btn btn-light" @click="saveChanges()">Save changes</button>
        </template>
      </div>
    </div>
  </div>
</template>

<script setup>
import "bootstrap";
import "bootstrap/dist/css/bootstrap.css";

import { reactive } from "vue";

//Drag and drop library
import { VueDraggableNext as draggable } from "vue-draggable-next";

//Sweetalert
import Swal from "sweetalert2";
import "sweetalert2/dist/sweetalert2.min.css";

//Array with the main elements list
const draggableElements = [
  {
    id: crypto.randomUUID(),
    visibleName: "Description",
    descriptionText: "This is a description that you can edit",
    type: "description",
    mandatory: false,
  },
];

//Variable where the data flows
const state = reactive({
  editedIndex: -1,
  contentElements: [],
  showJSONPanel: false,
  generatedJSON: "",
});

const edit = reactive({
  descriptionNewText: "",
});

function copyJSON() {
  const textarea = document.querySelector(".json-textarea");
  textarea.select();
  document.execCommand("copy");
  Swal.fire("Success", "JSON copied", "success");
}

function generateJSON() {
  state.generatedJSON = JSON.stringify(state.contentElements, null, 2);
}

// Handle drag end to clone elements properly
function onDragEnd(event) {
  if (event.added) {
    const newElement = JSON.parse(JSON.stringify(event.added.element));
    newElement.id = crypto.randomUUID();
    state.contentElements.splice(event.newIndex, 1, newElement);
    generateJSON();
  }
}

//Edit
function editElement(index) {
  state.editedIndex = index;
  const element = state.contentElements[index];
  if (element.type === 'description') {
    edit.descriptionNewText = element.descriptionText;
  }
}

//Delete element
function deleteElement(index) {
  Swal.fire({
    title: "Confirm delete",
    text: "Are you sure?",
    icon: "warning",
    showCancelButton: true,
    confirmButtonText: "Yes, delete",
    cancelButtonText: "Cancel",
  }).then((result) => {
    if (result.isConfirmed) {
      state.contentElements.splice(index, 1);
      generateJSON();
    }
  });
}

//Save changes in the element
function saveChanges() {
  const type = state.contentElements[state.editedIndex].type;
  switch(type){
    case 'description':
      state.contentElements[state.editedIndex].descriptionText = edit.descriptionNewText;
      break;
  }
  state.editedIndex = -1;
  edit.descriptionNewText = "";
}
</script>

<style scoped>
.drag-element {
  cursor: pointer;
}

.edit-icon {
  font-size: 25px;
}
.edit-icon:hover {
  color: blueviolet;
  cursor: pointer;
}
</style>

效果验证

  1. 拖拽多个Description元素到中间区域,每个元素均为独立对象,修改其中一个不会影响其他元素。
  2. 点击编辑按钮时,输入框会自动填充当前元素的文本内容。
  3. 删除按钮可精准删除目标元素,不影响其他元素。
  4. JSON面板可实时更新当前元素列表的JSON数据。

内容的提问来源于stack exchange,提问作者Juan Montoya

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 21:05:56