Vue.js多组件交互异常:关闭ProjectWindow后无法打开文件夹卡片
Vue组件交互异常修复方案
问题核心原因
- 直接操作DOM元素导致状态混乱,脱离Vue响应式系统,关闭ProjectWindow后DOM被
v-if销毁,后续获取元素失败 - ToolBar组件逻辑重叠,同时处理
card和projectWindow的DOM操作,引发状态冲突 - 透明度值设置错误:
opacity合法范围是0-1,而非代码中的10
解决方案
1. 重构ToolBar组件
移除所有DOM操作,改为通过事件通知父组件处理状态,让父组件完全控制显示逻辑,实现真正的组件复用。
2. 用响应式数据替代DOM操作
所有显示/隐藏、样式变化通过Vue的响应式数据控制,不再手动修改元素style。
3. 统一状态管理
由ProjectsView父组件统一管理文件夹卡片和ProjectWindow的显示、最小化状态,避免组件间状态干扰。
修改后的组件代码
ToolBar组件
<script> export default { name: 'ToolBar', emits: ['toggle-size', 'close'], props: { isMinimized: Boolean } } </script> <template> <div class="toolbar"> <h4 @click="$emit('toggle-size')" class="bi" :class="isMinimized ? 'bi-fullscreen' : 'bi-fullscreen-exit'"></h4> <h4 @click="$emit('close')" class="bi bi-x-square" style="color: darkred"></h4> </div> </template> <style lang="scss"> .toolbar { display: flex; flex-direction: row; margin-left: auto; gap: 20px; } </style>
ProjectWindow组件
<script> import axios from 'axios'; import { ref, onMounted } from 'vue'; import ToolBar from "@/components/ToolBar.vue"; export default { name: 'ProjectWindow', components: { ToolBar }, props: { slug: String, isMinimized: Boolean }, emits: ['close', 'toggle-size'], setup(props) { const projectData = ref({ name: null, slug: null, description: null, gitHub: null, isHosted: null, hostLink: null, }); onMounted(async () => { try { const response = await axios.get(`/api/v1/projects/`); const project = response.data.find(p => p.slug === props.slug); if (project) { projectData.value = { name: project.name, slug: project.slug, description: project.description.replace(/\\n/g, '<br>'), gitHub: project.github, isHosted: project.is_hosted, hostLink: project.host_link, }; } } catch (error) { console.error(error); } }); return { projectData, }; } }; </script> <template> <section class="container" :class="{ minimized: isMinimized }"> <div class="card"> <div class="card-header"> <h3 class="title">{{ projectData.name }}</h3> <ToolBar :is-minimized="isMinimized" @toggle-size="$emit('toggle-size')" @close="$emit('close')" /> </div> <div class="card-body" v-show="!isMinimized"> <div class="description" v-html="projectData.description"></div> </div> <div class="card-footer" v-show="!isMinimized" style="display: flex; flex-direction: row; gap: 10px"> <a :href="projectData.gitHub" target="_blank" class="btn btn-primary">GitHub</a> <a v-if="projectData.isHosted" :href="projectData.hostLink" target="_blank" class="btn btn-primary">Website Link</a> </div> </div> </section> </template> <style scoped> .container { transition: all 0.5s ease-in-out; } .container.minimized { height: 50px; overflow: hidden; margin-top: auto; } </style>
ProjectsView视图
<script> import ProjectWindow from "@/components/ProjectWindow.vue"; import ToolBar from "@/components/ToolBar.vue"; export default { name: 'Projects', components: {ToolBar, ProjectWindow}, data() { return { slug: null, isFolderOpen: false, isFolderMinimized: false, isGameOpen: false, isGameMinimized: false } }, methods: { openGame(slug) { this.slug = slug; this.isGameOpen = true; this.isFolderOpen = false; }, toggleFolderSize() { this.isFolderMinimized = !this.isFolderMinimized; }, closeFolder() { this.isFolderOpen = false; this.isFolderMinimized = false; }, toggleGameSize() { this.isGameMinimized = !this.isGameMinimized; }, closeGame() { this.isGameOpen = false; this.isGameMinimized = false; } } }; </script> <template> <div class="side-container"> <i class="bi" :class="isFolderOpen ? 'bi-folder2-open' : 'bi-folder2'" @click="isFolderOpen = !isFolderOpen" ></i> </div> <div v-if="isGameOpen"> <ProjectWindow :slug="slug" :is-minimized="isGameMinimized" @toggle-size="toggleGameSize" @close="closeGame" /> </div> <section class="games container" v-if="isFolderOpen" :class="{ minimized: isFolderMinimized }"> <div class="card"> <div class="card-header"> <h3 class="title">Games</h3> <ToolBar :is-minimized="isFolderMinimized" @toggle-size="toggleFolderSize" @close="closeFolder" /> </div> <div class="card-body" v-show="!isFolderMinimized"> <div class="game"> <label>Shoot The Crow</label> <i class="bi bi-bullseye" style="color: red" @click="openGame('shootthecrow')"></i> </div> <div class="game"> <label>Guess The Number</label> <i class="bi bi-patch-question" style="color: green" @click="openGame('guessthenumber')"></i> </div> <div class="game"> <label>The Pig Game</label> <i class="bi bi-dice-5-fill" style="color: mediumpurple" @click="openGame('piggame')"></i> </div> </div> </div> </section> </template> <style scoped> .games.container { transition: all 0.5s ease-in-out; } .games.container.minimized { height: 50px; overflow: hidden; margin-top: auto; } </style>
修改关键点说明
- ToolBar仅负责触发事件,不处理任何业务逻辑,适配不同父组件的需求
- 所有显示状态通过响应式变量控制,完全遵循Vue数据流原则
- 移除所有
document.getElementById调用,避免DOM销毁后获取失败的问题 - 打开ProjectWindow时自动关闭文件夹卡片,避免界面冲突
- 最小化效果通过CSS类实现,过渡动画更流畅
内容的提问来源于stack exchange,提问作者ZxPower
相关产品推荐
相关产品推荐

