【vue2+Element】多层结构上传文件时遇到的问题和解决方法

文章讲述了在Vue项目中使用ElementUI的文件上传组件时遇到的两个问题:一是文件上传组件钩子的v-foridx处理不当导致死循环,二是如何响应式地修改文件列表数据以保持页面同步。作者提供了详细的问题描述、原因分析和相应解决方案。

功能说明和具体实现

功能说明:

课程内容可以选择无章节和多章节上传,选择无章节课程则直接上传文件,选择多章节上传时要上传章节名对应的文件,和无章节不同的是,不能一层数据结构直接实现,需要设计两层数据结构,并且都是单文件上传,上传后上传按钮隐藏,移除上传文件后展示上传按钮

选择无章节课程:
无章节课程
选择多章节课程:
在这里插入图片描述
无章节课程上传后:
在这里插入图片描述
多章节课程上传后:
在这里插入图片描述

具体实现:

基本逻辑

通过v-if判断是无章节还是多章节,多章节时,数据结构如下:

 ruleForm: {
   .......
   multipleChapters: [
     { chapterName: '', coursewareType: '', coursewarePath: '', coursewareName: '', fileList: [] },
   ],

通过v-for遍历ruleForm的multipleChapters, 作为响应式数据绑定到DOM,初始化每个章节的章节名称和课程文件,添加和删除某个章节也是直接对ruleForm的multipleChapters进行操作

  <el-form-item label="课程内容:" prop="multipleChapter">
	  <el-radio v-model="ruleForm.multipleChapter" label="0">无章节课程</el-radio>
	   <el-radio v-model="ruleForm.multipleChapter" label="1">多章节课程</el-radio>
	   <div class="course-form-item">
	     <span style="font-weight: 600; margin-top: 10px">提示:</span>
	     <span>本地上传音视频最大支持3GB(文档最大支持200M),mp4、mp3、pdf格式文件</span>
	     <div v-if="ruleForm.multipleChapter == 1" class="multiple-button" >
	       <el-button style="margin: 20px 0 10px 0" round icon="el-icon-plus" @click="addNewSection">
	         添加章节
	       </el-button>
	       <div class="course-form-item-wrap" v-for="(item, idx) in ruleForm.multipleChapters" :key="idx">
	         <div class="course-form-item-name">
	           <el-input v-model="item.chapterName" placeholder="请输入章节名称"></el-input>
	           <i class="el-icon-delete" @click="delOneSection(idx)"></i>
	         </div>
	         <div class="course-form-item-upload">
	           <el-upload
	             class="avatar-uploader"
	             :file-list="ruleForm.multipleChapters[idx].fileList"
	             :on-remove="(file, fileList) => handleRemoveMultiple(file, fileList, idx)"
	             :show-file-list="true"
	             :multiple="false"
	             limit="1"
	             action="/upload"
	             :http-request="e => handleAvatarUploadDemo(e, 'cover2', idx)"
	             :before-upload="beforeVideoUpload"
	             :accept="accept">
	             <div v-if="item.fileList.length === 0">
	               <i style="color: #2a83cb" class="el-icon-upload2"></i>
	               <span style="color: #2a83cb">从本地上传</span>
	             </div>
	           </el-upload>
	         </div>
	       </div>
	     </div>
	     <div v-else>
	       <el-upload
	         class="course-form-item-single"
	         :file-list="coursewarePath1"
	         :on-remove="handleRemoveSingle"
	         :show-file-list="true"
	         :multiple="false"
	         limit="1"
	         action="/upload"
	         :http-request="e => handleAvatarUploadDemo(e, 'cover1')"
	         :before-upload="beforeVideoUpload"
	         :accept="accept">
	         <el-button
	           v-if="ruleForm.coursewarePath == ''"
	           style="margin: 20px 0 10px 0; display: block"
	           round
	           icon="el-icon-upload2">
	           本地上传
	         </el-button>
	       </el-upload>
	     </div>
	   </div>
 </el-form-item>

问题一:文件上传组件的钩子没有正确添加v-for的idx导致死循环

问题描述:
:on-remove="(file, fileList) => handleRemoveMultiple(file, fileList, idx)"

在移除文件时,需要用到v-for的idx,来操作具体移除具体哪个章节的课程文件,如果写成:on-remove=“handleRemoveMultiple(file, fileList, idx)”,handleRemoveMultiple中不能成功获取参数中的idx,多章节课程的radio无法点击,控制台打印显示出现死循环,只能在浏览器中关闭tab重新进入项目才能恢复正常
在这里插入图片描述

   :http-request="e => handleAvatarUploadDemo(e, 'cover2', idx)"

也是同样的情况

原因和解决方法:

查阅elementui官网:
在这里插入图片描述
移除文件的钩子默认有两个参数,如果要添加新的参数,要这样写,handleRemoveMultiple函数才能获取到idx的值,否则idx 为 undefined,可能会导致删除操作无法正确执行,从而进入死循环。

:on-remove="(file, fileList) => handleRemoveMultiple(file, fileList, idx)"

两种写法的区别:

  1. 箭头函数方式:
:on-remove="(file, fileList) => handleRemoveMultiple(file, fileList, idx)"

这种方式使用了箭头函数,箭头函数会绑定外层作用域的 this,因此在 handleRemoveMultiple 函数中可以访问到外部的 idx 变量

2.直接调用函数方式:

:on-remove="handleRemoveMultiple(file, fileList, idx)"

这种方式直接调用了 handleRemoveMultiple 函数,但是由于没有绑定外部作用域,handleRemoveMultiple 函数中无法访问到外部的 idx 变量,因此会导致 idx 为 undefined。

问题二:成功获取index后无法响应式修改文件列表数据

问题描述

以移除操作为例:

 handleRemoveMultiple(file, fileList, idx) {
   console.log('object', file, fileList, idx);
    this.$set(this.ruleForm.multipleChapters, idx, {
      chapterName: this.ruleForm.multipleChapters[idx].chapterName,
      coursewarePath: '',
      coursewareType: '',
      coursewareName: '',
      fileList: [],
    });
    // this.$set(this.ruleForm.multipleChapters[idx], 'fileList', []);
    // const index = this.ruleForm.multipleChapters.findIndex(item => item.coursewarePath === file.url);
},

修改数据时,如果直接这样修改:

  this.ruleForm.multipleChapters[idx].fileList = [];

控制台打印发现,数据是可以成功修改,但是DOM不能根据数据的变化及时更新,因为移除文件后需要重新展示上传按钮,这种情况下不能成功展示。

原因和解决方法

因为此时不是响应式修改数据,Vue 无法自动追踪到属性的变化从而更新页面展示,所以要响应式修改数据:

 handleRemoveMultiple(file, fileList, idx) {
      console.log('object', file, fileList, idx);
      this.$set(this.ruleForm.multipleChapters, idx, {
        chapterName: this.ruleForm.multipleChapters[idx].chapterName,
        coursewarePath: '',
        coursewareType: '',
        coursewareName: '',
        fileList: [],
      });
 }

这样数据不仅成功修改,页面也能成功更新

注意:
如果这样写:

 handleRemoveMultiple(file, fileList, idx) {
      console.log('object', file, fileList, idx);
      this.$set(this.ruleForm.multipleChapters, idx, {      
        coursewarePath: '',
        coursewareType: '',
        coursewareName: '',
        fileList: [],
      });
 }

章节的课程文件可以成功响应式修改了,但是章节对应的课程名称却会重置,因为 this.$set()对数组操作时,是重新替换掉数组某一项的全部数据,不会保留没有变化的数据,所以在操作时,要对章节名称也进行赋值,保证该字段不会被重置:

 this.$set(this.ruleForm.multipleChapters, idx, {
  chapterName: this.ruleForm.multipleChapters[idx].chapterName,
  ......
  });
<think>在Vue2Element UI中,同使用Dialog对话框Drawer抽屉组件,并且它们都可以操作(即同打开并进行交互)是常见的需求。需要注意的是,这两个组件都是模态的,默认情况下会有一个遮罩层(overlay),并且会阻止背景的滚动。当同存在多个模态组件Element UI会管理它们的叠放顺序(z-index)遮罩层的显示。 但是,如果同打开多个模态组件,遮罩层可能会叠加(默认情况下,每个模态组件都会有一个遮罩层),这可能会导致背景变暗。Element UI提供了一种方式来处理多个模态框的遮罩层:通过`modal-append-to-body``append-to-body`等属性来控制模态框遮罩层的插入位置。 对于DialogDrawer同存在的情况,我们需要注意以下几点: 1. **遮罩层处理**:为了避免多层遮罩导致背景过暗,我们可以设置后打开的组件的遮罩层为透明,或者只保留最上层组件的遮罩层。但是Element UI并没有直接提供这样的功能,所以我们可以通过以下方式处理: - 使用`:modal="false"`来禁用遮罩层(不推荐,因为这样用户仍然可以操作背景内容)。 - 或者,我们可以通过CSS覆盖的方式调整遮罩层的透明度,但要注意多个遮罩层叠加的问题2. **z-index管理**:确保后打开的组件z-index更高,这样它会显示在之前打开的组件之上。Element UI内部会自动管理,但如果你遇到覆盖问题,可以手动设置`z-index`属性。 3. **滚动条问题**:当多个模态框打开,可能会因为多个模态框都试图禁止背景滚动而导致冲突。Element UI在模态框打开会自动给body添加`overflow: hidden`,关闭移除。如果有多个模态框,那么只有当最后一个模态框关闭,滚动条才会恢复。 4. **多个模态框的交互**:用户可能期望点击遮罩层关闭最上层的模态框,而不是所有模态框。Element UI默认行为是点击遮罩层会关闭当前模态框。 下面是一个同使用DialogDrawer的示例: 我们有两个按钮,分别打开DialogDrawer。我们将使用两个状态变量`dialogVisible``drawerVisible`来控制它们的显示。 在同打开,我们希望它们都能正常操作,并且遮罩层不会叠加(这里我们采用只显示一个遮罩层的方式,即Drawer的遮罩层覆盖在Dialog的遮罩层之上,但由于Drawer的遮罩层是半透明的,所以底层遮罩层会使得背景更暗)。如果你不希望这样,可以尝试禁用其中一个的遮罩层,但这样可能会导致用户误操作背景内容。 另一种方案:我们只允许一个模态框打开,当打开另一个,自动关闭前一个。这取决于你的业务需求。 这里我们按照同打开两个模态框,并且都启用遮罩层的情况来写。 示例代码: ```vue <template> <div> <el-button type="primary" @click="dialogVisible = true">打开Dialog</el-button> <el-button type="primary" @click="drawerVisible = true">打开Drawer</el-button> <!-- Dialog --> <el-dialog title="提示" :visible.sync="dialogVisible" width="30%" :modal-append-to-body="true" :append-to-body="true" > <span>这是一个Dialog</span> <span slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="dialogVisible = false">确 定</el-button> </span> </el-dialog> <!-- Drawer --> <el-drawer title="我是标题" :visible.sync="drawerVisible" direction="rtl" :modal-append-to-body="true" :append-to-body="true" size="50%"> <span>这是一个Drawer</span> </el-drawer> </div> </template> <script> export default { data() { return { dialogVisible: false, drawerVisible: false }; } }; </script> ``` 在上面的代码中,我们给DialogDrawer都设置了`modal-append-to-body``append-to-body`为`true`(实际上,这两个属性默认值就是`true`,所以也可以不写)。这样,它们的遮罩层都会被插入到body元素下,这样多个模态框的遮罩层就不会相互干扰,并且z-index的管理也会更加合理。 但是,当两个模态框都打开,会有两个遮罩层,这样背景会变得非常暗。为了解决这个问题,我们可以通过CSS来调整遮罩层的透明度,或者只显示最上层的遮罩层。然而,Element UI并没有提供直接的API来控制遮罩层的叠加,所以我们可以通过动态调整遮罩层的样式来实现。 我们可以监听两个模态框的打开状态,当两个都打开,将非顶层的遮罩层设置为透明。但是,由于遮罩层都是插入到body下的,并且顺序打开顺序一致,所以我们可以通过CSS选择器将非最后一个的遮罩层设为透明。 添加全局样式(在组件的style标签中,注意要使用scoped的话,可能需要使用深度选择器,但遮罩层在body下,所以可以在全局样式中设置): 注意:在Vue组件中,可以使用非scoped样式或者使用全局样式表。 在组件的style标签中(非scoped): ```html <style> /* 当有多个遮罩层,除了最后一个,其他都透明 */ .el-overlay:not(:last-child) { background-color: transparent !important; } </style> ``` 但是,这种方法可能不总是可靠,因为遮罩层的顺序打开顺序有关,而且可能还有其他组件也使用了遮罩层。 另一种更可控的方法是:在打开Drawer,如果Dialog已经打开,那么我们就将Dialog的遮罩层隐藏(通过动态类名),反之亦然。但这样需要手动控制。 考虑到复杂度,如果项目要求同只能有一个模态框,那么可以在打开一个关闭另一个。例如: ```vue <template> <div> <el-button type="primary" @click="openDialog">打开Dialog</el-button> <el-button type="primary" @click="openDrawer">打开Drawer</el-button> <!-- Dialog --> <el-dialog title="提示" :visible.sync="dialogVisible" width="30%" > <span>这是一个Dialog</span> <span slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="dialogVisible = false">确 定</el-button> </span> </el-dialog> <!-- Drawer --> <el-drawer title="我是标题" :visible.sync="drawerVisible" direction="rtl" size="50%"> <span>这是一个Drawer</span> </el-drawer> </div> </template> <script> export default { data() { return { dialogVisible: false, drawerVisible: false }; }, methods: { openDialog() { this.drawerVisible = false; // 关闭抽屉 this.dialogVisible = true; }, openDrawer() { this.dialogVisible = false; // 关闭对话框 this.drawerVisible = true; } } }; </script> ``` 这样就能保证同只显示一个模态框。 如果你需要同显示两个模态框,并且不希望遮罩层叠加,那么可以尝试使用CSS调整,但需要谨慎处理。或者,你也可以考虑使用非模态的方式(即不使用遮罩层)来显示其中一个组件,但这可能会影响用户体验。 综上所述,根据你的需求选择合适的方式。 最后,如果你需要同显示DialogDrawer,并且都使用遮罩层,那么建议使用第一种方式(两个都打开),然后通过全局CSS调整遮罩层的透明度(比如只保留最上层遮罩层的背景色,其他的透明)。但是要注意,这样用户点击遮罩层,可能会关闭底层的模态框,而你可能希望关闭的是最上层的。Element UI默认点击遮罩层会关闭当前模态框,所以这个问题不大,因为每个遮罩层都绑定在各自的模态框上。 但是,由于多个遮罩层叠加,点击遮罩层可能会同触发多个关闭事件(因为多个遮罩层重叠在一起)。为了避免这种情况,我们可以通过CSS将非顶层的遮罩层设置为透明并且不响应鼠标事件(pointer-events: none)。这样只有最上层的遮罩层会响应点击事件。 修改后的CSS: ```css .el-overlay { pointer-events: auto; /* 确保遮罩层可以响应事件 */ } .el-overlay:not(:last-child) { background-color: transparent !important; pointer-events: none; /* 非最后一个遮罩层不响应事件 */ } ``` 这样,只有最后一个遮罩层(最上层)会响应点击事件,并且点击它会关闭对应的模态框,而底层的遮罩层则不会响应事件,因此底层的模态框不会关闭。 注意:这种方法依赖于遮罩层的DOM顺序,即后打开的遮罩层在DOM中排在后面(因为append到body)。而Element UI的遮罩层正是按照打开顺序依次追加到body的,所以最后一个遮罩层就是最后打开的组件对应的遮罩层。 因此,我们可以在全局样式(例如App.vue)中设置上述样式,这样整个应用中的多个模态框都可以按照这个规则。 总结:在Vue2+Element UI中同使用DialogDrawer,并且希望它们可以同操作,可以通过以下步骤: 1. 使用`modal-append-to-body``append-to-body`(默认就是true,所以可以不写)确保遮罩层插入到body。 2. 使用全局CSS控制非顶层的遮罩层透明且不响应事件。 这样,用户就可以同操作两个模态框,并且点击遮罩层只会关闭最上层的模态框。 注意:在关闭一个模态框后,需要将之前隐藏的遮罩层重新显示(实际上是自动调整的,因为关闭后对应的遮罩层会被移除,所以剩下的遮罩层中最后一个又会成为顶层)。 最后,根据你的实际需求,选择同打开还是互斥打开。 示例代码(同打开两个,并处理遮罩层): 在App.vue的全局样式中添加: ```css /* 全局有效 */ .el-overlay:not(:last-child) { background-color: transparent !important; pointer-events: none; } ``` 组件内: ```vue <template> <div> <el-button type="primary" @click="dialogVisible = true">打开Dialog</el-button> <el-button type="primary" @click="drawerVisible = true">打开Drawer</el-button> <el-dialog title="提示" :visible.sync="dialogVisible" width="30%" > <span>这是一个Dialog</span> <span slot="footer" class="dialog-footer"> <el-button @click="dialogVisible = false">取 消</el-button> <el-button type="primary" @click="dialogVisible = false">确 定</el-button> </span> </el-dialog> <el-drawer title="我是标题" :visible.sync="drawerVisible" direction="rtl" size="50%"> <span>这是一个Drawer</span> </el-drawer> </div> </template> <script> export default { data() { return { dialogVisible: false, drawerVisible: false }; } }; </script> ``` 这样,当两个都打开,Dialog先打开,Drawer后打开,那么Drawer的遮罩层会覆盖在Dialog上,并且Dialog的遮罩层透明且不响应事件,所以点击Drawer遮罩层会关闭Drawer,而Dialog的遮罩层由于被覆盖且不响应事件,所以不会关闭Dialog。关闭Drawer后,Dialog的遮罩层又会成为顶层,恢复事件响应。 注意:如果两个组件打开的顺序不确定,那么需要确保后打开的组件遮罩层在DOM中排在后面,这样CSS规则才会生效。而Element UI正是这样做的。 因此,这种方式可以满足需求。 如果遇到问题,可能需要调整CSS选择器或者检查遮罩层的DOM结构。 相关问题:</think>在 Vue2 + Element UI 中同使用 Dialog 对话框 Drawer 抽屉组件,需要特别注意它们的遮罩层管理、状态控制布局冲突。以下是完整的解决方案: ### 解决方案代码 ```vue <template> <div> <!-- 操作按钮 --> <el-button @click="showDialog = true">打开对话框</el-button> <el-button @click="showDrawer = true">打开抽屉</el-button> <!-- Dialog 对话框 --> <el-dialog title="操作对话框" :visible.sync="showDialog" width="50%" :modal="true" :close-on-click-modal="false" :append-to-body="true" > <div> <el-input v-model="dialogInput" placeholder="在对话框中输入内容"></el-input> <el-button @click="showDrawerFromDialog = true">在对话框中打开抽屉</el-button> </div> <span slot="footer"> <el-button @click="showDialog = false">取消</el-button> <el-button type="primary" @click="handleDialogConfirm">确认</el-button> </span> </el-dialog> <!-- Drawer 抽屉 --> <el-drawer title="操作抽屉" :visible.sync="showDrawer" size="40%" direction="rtl" :modal="true" :append-to-body="true" > <div> <el-select v-model="drawerSelect" placeholder="在抽屉中选择"> <el-option label="选项1" value="1"></el-option> <el-option label="选项2" value="2"></el-option> </el-select> <el-button @click="showDialogFromDrawer = true">在抽屉中打开对话框</el-button> </div> </el-drawer> <!-- 嵌套组件 --> <el-dialog v-if="showDialogFromDrawer" title="从抽屉打开的对话框" :visible.sync="showDialogFromDrawer" width="40%" append-to-body > <p>这是从抽屉中打开的对话框</p> </el-dialog> <el-drawer v-if="showDrawerFromDialog" title="从对话框打开的抽屉" :visible.sync="showDrawerFromDialog" size="30%" append-to-body > <p>这是从对话框中打开的抽屉</p> </el-drawer> </div> </template> <script> export default { data() { return { showDialog: false, // 控制对话框显示 showDrawer: false, // 控制抽屉显示 dialogInput: '', // 对话框输入内容 drawerSelect: '', // 抽屉选择内容 showDialogFromDrawer: false, // 从抽屉中打开对话框 showDrawerFromDialog: false // 从对话框中打开抽屉 }; }, methods: { handleDialogConfirm() { this.$message.success(`对话框输入内容: ${this.dialogInput}`); this.showDialog = false; } } }; </script> <style scoped> /* 防止滚动条闪烁 */ body { overflow: hidden !important; } /* 调整层级关系 */ .el-dialog__wrapper { z-index: 2000 !important; } .el-drawer__wrapper { z-index: 2001 !important; } </style> ``` ### 代码解释 1. **状态管理** - 使用独立的 `showDialog` `showDrawer` 控制显示状态 - 添加嵌套状态 `showDialogFromDrawer` `showDrawerFromDialog` 处理组件间调用 2. **关键属性** - `append-to-body="true"`:将组件添加到 body 末尾,避免嵌套问题 - `:modal="true"`:显示遮罩层 - `:close-on-click-modal="false"`:防止误触遮罩关闭 - `direction="rtl"`:抽屉从右侧弹出 3. **层级管理** - 通过 CSS 设置抽屉的 z-index (2001) 高于对话框 (2000) - 使用 `body { overflow: hidden }` 防止滚动条闪烁 4. **嵌套交互** - 在对话框中添加打开抽屉的按钮 - 在抽屉中添加打开对话框的按钮 - 使用独立的嵌套组件避免状态冲突 5. **样式调整** - 使用 scoped CSS 避免全局污染 - 通过 !important 覆盖 Element UI 默认样式 ### 最佳实践 1. **层级管理**:始终确保后打开的组件有更高的 z-index 2. **状态隔离**:为每个弹出组件使用独立的状态变量 3. **避免嵌套过深**:超过三层嵌套应考虑重构为独立页面 4. **性能优化**:使用 `v-if` 替代 `v-show` 减少 DOM 节点 5. **响应式设计**:在不同屏幕尺寸下调整 width/size 属性 ### 常见问题解决 1. **遮罩层重叠问题**: ```css .el-overlay { pointer-events: none; } .el-overlay:last-child { pointer-events: auto; } ``` 2. **滚动穿透问题**: ```js watch: { showDialog(val) { document.body.style.overflow = val ? 'hidden' : 'auto' }, showDrawer(val) { document.body.style.overflow = val ? 'hidden' : 'auto' } } ``` 3. **动画冲突**: ```vue <el-drawer :withHeader="false" :wrapperClosable="false"> ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值