Vue3列表元素编辑相互影响问题求助(用vue-draggable-next等组件)
问题解决:实现拖拽元素的独立编辑功能
问题根源
- 克隆元素引用重复:拖拽克隆时直接复用原元素对象,导致多个元素共享同一引用,修改一个会影响所有同来源克隆元素。
- 删除按钮未传索引:
deleteElement调用时未传递当前元素的index,无法精准删除目标元素。 - 编辑未同步当前值:点击编辑按钮后,输入框未填充当前元素的现有内容,用户体验差。
- 响应式变量错误嵌套:
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>
效果验证
- 拖拽多个
Description元素到中间区域,每个元素均为独立对象,修改其中一个不会影响其他元素。 - 点击编辑按钮时,输入框会自动填充当前元素的文本内容。
- 删除按钮可精准删除目标元素,不影响其他元素。
- JSON面板可实时更新当前元素列表的JSON数据。
内容的提问来源于stack exchange,提问作者Juan Montoya
相关产品推荐
相关产品推荐

