这段代码有个问题,多次打开弹窗数据数据,最后展示在的数据全部变成最后一次弹窗输入的内容,是什么原因,如何解决 - CSDN文库",
"datePublished": "2025-07-21",
"keywords": "
标题
模块
子项目列表
修改
详情
删除
新增子项目
提交
返回
这段代码有个问题,多次打开弹窗数据数据,最后展示在的数据全部变成最后一次弹窗输入的内容,是什么原因,如何解决",
"description": "文章浏览阅读3次。问题原因分析: 在代码中,当点击'新增子项目'时,会打开一个弹窗(smallTecAdd组件)。每次在弹窗中填写数据并提交时,会触发`handleChildEvent`事件,将子组件的数据`smallFormData`添加到`techigligAdd.smalltecList`数组中"
}
首页<!--
* @Author: luoxianbo [email protected]
* @Date: 2025-07-12 11:22:05
* @LastEditors: luoxianbo [email protected]
* @LastEditTime: 2025-07-21 22:14:32
* @FilePath: \VUE3-PROJECT\vite-project\src\components\tecHigLin\tecHigLinAdd.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div>
<div class="form-group">
<span>标题</span>
<el-input
v-model="techigligAdd.name"
style="width: 240px"
placeholder="Pick a date"
:suffix-icon="Calendar"
/>
</div>
<div class="form-group">
<span>模块</span>
<el-select
v-model="techigligAdd.type"
filterable
clearable
placeholder
@change="xlxx"
>
<el-option
v-for="item in selectypeList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</div>
<div class="form-group-textarea">
<label for="pbimessage">问题内容</label>
<!-- 双向绑定的富文本编辑器 -->
<QuillEditor
ref="quillEditor"
v-model:content="techigligAdd.techigligmessage"
contentType="html"
:options="editorOptions"
@blur="onEditorBlur"
/>
</div>
<div>
子项目列表
<el-table :data="techigligAdd.smalltecList" border style="width: 100%">
<el-table-column
prop="title"
label="标题"
width="180"
show-overflow-tooltip
/>
<el-table-column
prop="easytitle"
label="简述"
width="180"
show-overflow-tooltip
/>
<el-table-column
prop="httpurl"
label="相关博客"
show-overflow-tooltip="false"
/>
<el-table-column fixed="right" label="操作" min-width="120">
<template #default>
<el-button type="success">修改</el-button>
<el-button type="primary">详情</el-button>
<el-button type="danger">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="add-button-container" @click="openDialog">新增子项目</div>
<div class="tjbut">
<el-button type="success" @click="addBut" round>提交</el-button>
<el-button type="info" @click="goback" round>返回</el-button>
</div>
<!-- 弹窗表单 -->
<el-dialog
v-model="dialogVisible"
title="添加新项目"
width="500px"
:before-close="handleClose"
>
<smallTecAdd
@smallTecAdd-Event="handleChildEvent"
@close-el-dialog-component="closeDialog"
/>
</el-dialog>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from "vue";
import tecApi from "../../api/tecHigLinIndex.js";
import { useRouter } from "vue-router";
import { QuillEditor } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { useUserStore } from "../../../store/user.js";
import technologyTypeApi from "../../api/technologyTypeIndex.js";
import smallTecAdd from "../editor/SmallTecAdd.vue";
const isShowjsz = ref(false);
const router = useRouter();
// 用于保存 Quill 编辑器组件实例
const quillEditor = ref();
//表单引用
const formRef = ref(null);
//表单数据模型
const formData = reactive({
tecid: "",
title: "",
message: "",
httpurl: "",
easytitle: "",
});
//这里添加相关规则
const formRules = reactive([]);
//弹窗控制标识
const dialogVisible = ref(false);
const selectypeList = ref([
// { id: "1", name: "mysql" },
// { id: "2", name: "rabbitmq" },
// { id: "3", name: "springboot" },
// { id: "4", name: "nacos" },
]);
onMounted(() => {
dialogVisible.value = false;
getTecXiaLa();
});
//获取下拉列表接口
const getTecXiaLa = async () => {
const technologyTypeDTO = {};
const res = await technologyTypeApi.selectNoPage(technologyTypeDTO);
selectypeList.value = res.data.data;
};
//新增输入数据承接对象
const techigligAdd = reactive({
name: "",
techigligmessage: "",
type: "",
smalltecList: [],
});
//点击加号,展示输入项
const xlxx = () => {
isShowjsz.value = true;
};
// 图片上传处理函数
const imageHandler = async () => {
//创建一个隐藏的<input元素>
const input = document.createElement("input");
input.setAttribute("type", "file");
//且只接收图片
input.setAttribute("accept", "image/*");
input.click();
input.onchange = async () => {
//获取选择的第一个文件
const file = input.files[0];
//如果文件不存在,即为null,undefined或者空值时,直接结束函数
if (!file) {
return;
}
try {
// ✅ 调用后端图片上传接口
const formData = new FormData();
formData.append("file", file);
formData.append("fileType", "1");
const response = await api.fileUpload(formData);
if (response.data.code == 200) {
// ✅ 使用组件 ref 获取 Quill 实例
const quill = quillEditor.value.getQuill();
const range = quill.getSelection(true);
// ✅ 插入图片
const wzpath = useUserStore.nginxBasePath + response.data.data.filePath;
console.log("完整访问路径:" + wzpath);
quill.insertEmbed(range.index, "image", wzpath);
quill.setSelection(range.index + 1);
}
} catch (error) {
console.error("图片上传失败:", error);
}
};
};
const addBut = () => {
tecApi.addTec(techigligAdd).then((res) => {
console.log(res.data);
});
};
const goback = () => {
router.push({
name: "TecHigligListName",
});
};
// 编辑器配置选项
const editorOptions = ref({
modules: {
toolbar: {
container: [
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
["link", "image", "video"],
["clean"],
],
handlers: {
// 自定义图片上传处理
image: imageHandler,
},
},
},
placeholder: "请输入内容...",
theme: "snow",
});
// 编辑器失焦事件
const onEditorBlur = (event) => {
console.log("编辑器内容:", event.value);
};
//点击加号,打开一个弹窗
const openDialog = () => {
dialogVisible.value = true;
//如果表单有内容,则重置表单
if (formRef.value) {
formRef.value.resetFields();
}
};
//在关闭弹窗前确认,可以提供关闭拦截的作用
const handleClose = (done) => {
done();
};
//处理子组件传递的数据
const handleChildEvent = (smallFormData) => {
console.log(smallFormData);
//将数据添加到数组的末尾
techigligAdd.smalltecList.push(smallFormData);
};
const closeDialog = () => {
dialogVisible.value = false;
};
</script>
<style scoped lang="scss">
.ql-editor {
min-height: 300px;
}
.tjbut {
margin-top: 20px;
}
/* 下面这部分是加号按钮图标的css样式 */
.add-button-container {
display: flex;
align-items: left;
justify-content: center;
min-height: 50px;
border: 1px dashed #891818;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.add-button-container:hover {
background-color: #54b34b;
}
/* 弹窗相关css */
.el-button {
margin: 10px;
}
/* 自定义弹窗样式 */
:deep(.el-dialog) {
border-radius: 12px;
width: 80%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
:deep(.el-dialog__header) {
border-bottom: 1px solid #eee;
padding: 15px 20px;
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-form-item) {
margin-bottom: 18px;
}
</style>
这段代码有个问题,多次打开弹窗数据数据,最后展示在<el-table>的数据全部变成最后一次弹窗输入的内容,是什么原因,如何解决 <think>问题原因分析:
在代码中,当点击“新增子项目”时,会打开一个弹窗(smallTecAdd组件)。每次在弹窗中填写数据并提交时,会触发`handleChildEvent`事件,将子组件的数据`smallFormData`添加到`techigligAdd.smalltecList`数组中。
但是,由于`smallFormData`是子组件中的一个响应式对象(很可能是使用`reactive`或`ref`创建的),每次传递的都是同一个对象的引用。当再次打开弹窗时,子组件中的表单数据并没有被重置,因此当用户再次输入时,修改的是同一个对象,导致之前添加到数组中的项也会跟着改变。
解决方案:
1. 在父组件接收子组件传递的数据时,进行深拷贝,创建一个新的对象,这样数组中的每个元素都是独立的对象。
2. 在子组件中,每次提交后重置表单数据,确保下一次打开时是新的数据。
3. 在父组件中,每次打开弹窗时,强制重新创建子组件(使用`v-if`控制),这样每次打开都是一个新的实例,表单数据自然重置。
这里我们采用第一种方法(深拷贝)并配合第二种方法(子组件重置)来确保数据独立。
具体修改步骤:
步骤1:修改父组件中的`handleChildEvent`方法,对子组件传递过来的数据进行深拷贝。
步骤2:在子组件中,当提交事件触发后,重置表单数据。
由于我们无法修改子组件的代码(题目中未给出子组件代码),因此我们先在父组件中采用深拷贝的方式。
修改父组件的`handleChildEvent`方法:
将:
techigligAdd.smalltecList.push(smallFormData);
改为:
// 深拷贝一份数据
const newData = JSON.parse(JSON.stringify(smallFormData));
techigligAdd.smalltecList.push(newData);
但是,这种方法对于包含函数、undefined等特殊类型的数据不友好,但在这个场景中,表单数据都是基本类型或普通对象,所以可以使用。
步骤3:为了确保子组件在下次打开时是初始状态,我们可以在父组件中使用`v-if`来重新创建子组件。修改弹窗部分代码,给`smallTecAdd`组件加上`v-if="dialogVisible"`,这样当弹窗关闭时组件销毁,打开时重新创建。
修改后的弹窗部分代码:
<el-dialog ...>
<smallTecAdd
v-if="dialogVisible"
@smallTecAdd-Event="handleChildEvent"
@close-el-dialog-component="closeDialog"
/>
</el-dialog>
这样每次打开弹窗,子组件都会重新创建,表单数据也会初始化。
因此,我们做两处修改:
1. 在`handleChildEvent`方法中,使用深拷贝。
2. 在弹窗内的子组件上添加`v-if="dialogVisible"`。
修改后的代码示例:
在`<script setup>`部分修改`handleChildEvent`:
```javascript
const handleChildEvent = (smallFormData) => {
console.log(smallFormData);
// 深拷贝数据,然后添加到数组
const newItem = JSON.parse(JSON.stringify(smallFormData));
techigligAdd.smalltecList.push(newItem);
};
```
修改模板中的弹窗部分:
```html
<el-dialog
v-model="dialogVisible"
title="添加新项目"
width="500px"
:before-close="handleClose"
>
<smallTecAdd
v-if="dialogVisible"
@smallTecAdd-Event="handleChildEvent"
@close-el-dialog-component="closeDialog"
/>
</el-dialog>
```
注意:深拷贝可能会丢失一些特殊类型(如Date对象)的数据,但在当前表单数据中,通常都是字符串,所以问题不大。如果表单中有复杂对象,可以使用更健壮的深拷贝方法(如lodash的`_.cloneDeep`)。
另外,如果子组件内部没有重置表单,即使重新创建组件,如果用户打开弹窗后输入数据,然后取消关闭(不提交),再次打开时数据仍然存在。但因为我们每次打开弹窗都会重新创建子组件,所以数据会被重置。
如果子组件内部有状态需要重置,重新创建组件会重置所有状态,所以这种方式是有效的。
如果遇到性能问题(比如子组件很大,重新创建开销大),则可以考虑在子组件内部提供重置方法,然后在父组件打开弹窗时调用子组件的重置方法。但在这个场景中,重新创建子组件是简单有效的方法。
因此,按照上述修改即可解决数据重复的问题。
修改后的完整代码(仅修改部分):</think>你的代码中存在一个常见的响应式数据引用问题,导致多次打开弹窗后表格数据全部变成最后一次输入的内容。根本原因是子组件 `smallTecAdd` 中的表单数据对象是同一个引用,每次提交时修改的是同一个对象。
### 问题原因分析
1. **引用共享问题**:
- 子组件 `smallTecAdd` 中的表单数据对象是同一个响应式对象
- 每次提交时,`handleChildEvent` 接收的都是同一个对象的引用
- 当再次打开弹窗修改表单时,所有已添加的项都会更新
2. **对象复用问题**:
- 子组件没有在每次打开时重置表单状态
- 父组件没有创建新的数据副本
- 表格中所有行都指向同一个内存地址
3. **响应式系统特性**:
- Vue 的响应式系统会追踪对象引用
- 修改对象属性会影响所有引用该对象的地方
- 表格渲染的是同一个对象的多个引用
### 解决方案
#### 方法 1:深拷贝数据(推荐)
修改 `handleChildEvent` 方法,创建数据副本:
```javascript
const handleChildEvent = (smallFormData) => {
// 创建深拷贝副本
const newItem = JSON.parse(JSON.stringify(smallFormData));
techigligAdd.smalltecList.push(newItem);
};
```
#### 方法 2:强制重建子组件
修改弹窗部分代码,添加 `v-if`:
```html
<el-dialog
v-model="dialogVisible"
title="添加新项目"
width="500px"
:before-close="handleClose"
>
<!-- 添加 v-if 强制重建 -->
<smallTecAdd
v-if="dialogVisible"
@smallTecAdd-Event="handleChildEvent"
@close-el-dialog-component="closeDialog"
/>
</el-dialog>
```
#### 方法 3:在子组件中重置状态
修改 `smallTecAdd.vue` 组件(假设你有访问权限):
```vue
<script setup>
import { reactive } from "vue";
const formData = reactive({
title: "",
easytitle: "",
httpurl: "",
});
const emit = defineEmits(["smallTecAdd-Event", "close-el-dialog-component"]);
const submitForm = () => {
// 传递副本而非引用
emit("smallTecAdd-Event", { ...formData });
// 重置表单
formData.title = "";
formData.easytitle = "";
formData.httpurl = "";
emit("close-el-dialog-component");
};
</script>
```
### 完整修复代码
```html
<template>
<!-- ... 其他代码不变 ... -->
<el-dialog
v-model="dialogVisible"
title="添加新项目"
width="500px"
:before-close="handleClose"
>
<!-- 添加 v-if 强制重建 -->
<smallTecAdd
v-if="dialogVisible"
@smallTecAdd-Event="handleChildEvent"
@close-el-dialog-component="closeDialog"
/>
</el-dialog>
</template>
<script setup>
// ... 其他代码不变 ...
const handleChildEvent = (smallFormData) => {
// 深拷贝创建新对象
const newItem = JSON.parse(JSON.stringify(smallFormData));
techigligAdd.smalltecList.push(newItem);
};
</script>
```
### 为什么这种方法有效?
1. **深拷贝**:
- 使用 `JSON.parse(JSON.stringify())` 创建全新对象
- 切断与原始对象的引用关系
- 确保每次添加都是独立的数据项
2. **组件重建**:
- `v-if="dialogVisible"` 在弹窗关闭时销毁组件
- 再次打开时创建全新的组件实例
- 表单状态自然重置
3. **响应式系统**:
- Vue 能正确追踪新对象的变更
- 表格渲染的是独立的数据项
### 最佳实践建议
1. **使用更健壮的深拷贝**:
```javascript
import { cloneDeep } from 'lodash-es';
const handleChildEvent = (smallFormData) => {
techigligAdd.smalltecList.push(cloneDeep(smallFormData));
};
```
2. **添加唯一标识**:
```javascript
const handleChildEvent = (smallFormData) => {
const newItem = {
...cloneDeep(smallFormData),
id: Date.now() // 添加唯一标识
};
techigligAdd.smalltecList.push(newItem);
};
```
3. **实现编辑功能**:
```javascript
const editItem = (index) => {
// 将数据传递给子组件
currentEditIndex.value = index;
formDataToEdit.value = cloneDeep(techigligAdd.smalltecList[index]);
dialogVisible.value = true;
};
const saveEdit = (updatedData) => {
techigligAdd.smalltecList[currentEditIndex.value] = cloneDeep(updatedData);
};
```
### 完整优化后的代码
```html
<template>
<!-- ... 其他代码不变 ... -->
<el-table :data="techigligAdd.smalltecList" border style="width: 100%">
<!-- ... 列定义 ... -->
<el-table-column fixed="right" label="操作" min-width="120">
<template #default="scope">
<el-button type="success" @click="editItem(scope.$index)">修改</el-button>
<el-button type="danger" @click="deleteItem(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="dialogVisible"
:title="isEditing ? '编辑项目' : '添加新项目'"
width="500px"
>
<smallTecAdd
v-if="dialogVisible"
:initialData="formDataToEdit"
@save-data="handleSaveData"
@close-dialog="closeDialog"
/>
</el-dialog>
</template>
<script setup>
import { reactive, ref, onMounted } from "vue";
import { cloneDeep } from 'lodash-es';
// ... 其他代码不变 ...
const isEditing = ref(false);
const currentEditIndex = ref(-1);
const formDataToEdit = ref(null);
const openDialog = () => {
isEditing.value = false;
formDataToEdit.value = null;
dialogVisible.value = true;
};
const editItem = (index) => {
isEditing.value = true;
currentEditIndex.value = index;
formDataToEdit.value = cloneDeep(techigligAdd.smalltecList[index]);
dialogVisible.value = true;
};
const deleteItem = (index) => {
techigligAdd.smalltecList.splice(index, 1);
};
const handleSaveData = (formData) => {
if (isEditing.value) {
techigligAdd.smalltecList[currentEditIndex.value] = cloneDeep(formData);
} else {
techigligAdd.smalltecList.push(cloneDeep(formData));
}
closeDialog();
};
const closeDialog = () => {
dialogVisible.value = false;
isEditing.value = false;
currentEditIndex.value = -1;
formDataToEdit.value = null;
};
</script>
```
### 注意事项
1. **深拷贝的局限性**:
- `JSON.parse(JSON.stringify())` 无法复制函数、undefined 等
- 对于复杂对象,使用 lodash 的 `cloneDeep`
2. **性能考虑**:
- 大型数组的深拷贝可能影响性能
- 考虑使用浅拷贝+手动复制嵌套对象
3. **组件设计**:
- 子组件应设计为纯展示组件
- 状态管理应放在父组件或 store 中