VUE实现多个弹窗优先级变化实现思路
在开发复杂的单页应用(SPA)时,我们经常会遇到需要管理多个浮动窗口(或称“弹窗”、“面板”)的场景。一个核心的用户体验要求是:用户当前操作的窗口应该总是在最顶层。本文将结合代码示例,总结一种在 Vue 3 (Composition API) 和 TypeScript 环境下,实现这一功能的清晰、可扩展的思路。
核心思路
该功能的实现主要依赖于三个关键部分:
- 集中式状态管理:使用一个响应式对象统一管理所有窗口的 z-index 层级。
- 点击置顶:当用户点击某个窗口时,动态提升其 z-index 到最高。
- 新窗口置顶:当一个新窗口被打开时,自动将其 z-index 设置为最高
实现步骤详解
1. 状态设计:统一管理 z-index
首先,我们需要一个地方来存储和跟踪所有浮动窗口的层级状态。在 Vue 3 的 setup 函数中,使用 reactive API 是一个绝佳的选择,因为它创建了一个响应式对象,任何对此对象的修改都会自动触发 UI 更新。
import { reactive } from 'vue';// --- 浮动窗口层级管理 ---
const windowZIndices = reactive<Record<string, number>>({cockpitBox: 10,realTimeWarning: 10,monitorBox: 10,historyEvent: 10,sampleNorth: 10,blackAndWhiteList: 10,shipList: 10,// ... 其他窗口
});
- reactive:确保了当 z-index 值变化时,视图能够自动重新渲染。
- Record<string, number>:这是一个 TypeScript 类型,定义了一个键是字符串(窗口名)、值是数字(z-index)的对象。
- 初始值:所有窗口的初始 z-index 都设为 10,表示它们在初始状态下层级相同
接着,在模板中,我们将每个窗口组件的 style 属性与这个 reactive 对象中的相应值进行绑定
<!-- 模板部分 -->
<div class="cockpitBox" @mousedown="(e) => bringToFront(e, 'cockpitBox')":style="{ zIndex: windowZIndices['cockpitBox'] }"><!-- ... -->
</div><RealTimeWarning v-if="store.showRealTimeWarning"@mousedown="(e) => bringToFront(e, 'realTimeWarning')":style="{ zIndex: windowZIndices['realTimeWarning'] }" /><ShipList class="shipList" v-if="store.showShipList"@mousedown="(e) => bringToFront(e, 'shipList')":style="{ zIndex: windowZIndices['shipList'] }" /><!-- ... 其他窗口组件 -->
2. 核心逻辑:bringWindowToFront 函数
这是实现“点击置顶”功能的核心。当一个窗口需要被置顶时,我们需要找到当前所有窗口中的最大 z-index,然后将目标窗口的 z-index 设置为这个最大值加一。
/*** 将指定名称的窗口置于顶层(z-index 最高)。* @param windowName 要置顶的窗口名称*/
const bringWindowToFront = (windowName: keyof typeof windowZIndices) => {// 1. 获取当前所有 z-index 值的最大值const maxZIndex = Math.max(...Object.values(windowZIndices));// 2. 将目标窗口的 z-index 设为最大值 + 1windowZIndices[windowName] = maxZIndex + 1;
};
为了在用户点击时调用它,我们为每个窗口绑定了 @mousedown 事件,该事件会调用一个简单的包装函数 bringToFront
const bringToFront = (event: MouseEvent, windowName: keyof typeof windowZIndices) => {bringWindowToFront(windowName);
};
3. 自动管理:新开窗口置顶
除了点击置顶,新打开的窗口也应该自动显示在最前面。这个功能通过 watch API 来实现,我们侦听控制每个窗口可见性的状态(通常是 Pinia store 中的一个布尔值)。
import { watch } from 'vue';const setupWindowManagement = () => {// 定义需要管理的窗口及其对应的 store 状态const windowsToManage: Record<string, () => boolean> = {realTimeWarning: () => store.showRealTimeWarning,monitorBox: () => store.showMonitor,historyEvent: () => store.showHistory,// ... 其他由 store 控制显隐的窗口};// 遍历并为每个窗口设置 watch 侦听器
//Object.prototype.hasOwnProperty.call用于判断一个属性是否是对象自身的属性for (const windowName in windowsToManage) {if (Object.prototype.hasOwnProperty.call(windowsToManage, windowName)) {const typedWindowName = windowName as keyof typeof windowZIndices;watch(windowsToManage[typedWindowName], (newValue, oldValue) => {// 当窗口从“不显示”变为“显示”时if (newValue && !oldValue) {// 调用置顶函数bringWindowToFront(typedWindowName);}});}}
};
最后,在 onMounted生命周期钩子中调用 setupWindowManagement(),即可在组件挂载后激活这些侦听器。
总结
通过结合 reactive 状态、事件处理和 watch 侦听器,我们构建了一个清晰、高效且易于维护的浮动窗口层级管理系统:
- reactive 对象:作为单一数据源,集中管理所有窗口的 z-index。
- @mousedown 事件:响应用户的直接交互,提供即时的“点击置顶”反馈。
- watch 侦听器:自动化处理程序状态变化(如窗口的显示/隐藏),确保新窗口始终拥有最高优先级。
这种方法不仅代码结构清晰,而且扩展性强。当需要添加新的浮动窗口时,只需在 windowZIndices 对象和 windowsToManage 映射中增加相应的条目即可,无需改动核心逻辑。
tips
1.for...in 循环
- 定义:for...in 是 JavaScript 中用于遍历对象属性的一种循环。它会遍历一个对象上所有可枚举的属性(包括自有属性和从原型链上继承的属性)。
- 作用:在这个场景下,它会依次遍历 windowsToManage 对象的每一个键(key)。
- 第一次循环,windowName 的值是字符串 'realTimeWarning'。
- 第二次循环,windowName 的值是字符串 'monitorBox'。
- ...以此类推,直到所有窗口都遍历完。
- 目的:通过这个循环,我们可以为每一个在 windowsToManage 中配置的窗口都应用上相同的逻辑(也就是给它们都设置一个 watch 侦听器)。
2. Object.prototype.hasOwnProperty.call()
hasOwnProperty 是什么?
- 每个 JavaScript 对象都有一个 hasOwnProperty('propertyName') 方法,它用来判断一个属性是对象自身的属性,还是从原型链上继承来的。如果是自身的,返回 true;如果是继承的,返回 false。
- 为什么需要它?
- for...in 循环有一个特点,它不仅会遍历对象自身的属性,还会遍历其原型链上的属性。在绝大多数情况下,我们只关心对象自身的属性。这个 if 判断就是为了过滤掉那些可能存在的、我们不关心的继承属性。
- 为什么不直接写 windowsToManage.hasOwnProperty(windowName)?
- 直接写 windowsToManage.hasOwnProperty(...) 在 99% 的情况下是没问题的。但 Object.prototype.hasOwnProperty.call(...) 是一个更安全、更健壮的写法,主要为了防止两种极端情况:
- 对象重写了 hasOwnProperty:如果 windowsToManage 对象恰好有一个自己的属性也叫 hasOwnProperty,那么直接调用就会执行被重写的版本,可能导致非预期的结果。
- 对象没有 hasOwnProperty 方法:如果一个对象是通过 Object.create(null) 创建的,那么它没有任何原型,也就不存在 hasOwnProperty 方法,直接调用会报错。
- .call() 的作用:
- call 允许我们调用一个函数,并且手动指定这个函数内部的 this 指向。
- Object.prototype.hasOwnProperty.call(windowsToManage, windowName) 的意思是:
- 找到 Object 原型上最原始、最正宗的那个 hasOwnProperty 方法。
- 通过 .call() 来执行它。
- 第一个参数 windowsToManage 是告诉 hasOwnProperty:“请把 this 当作是 windowsToManage 对象来执行”。
- 第二个参数 windowName 是传递给 hasOwnProperty 的参数。
- 这样就保证了无论 windowsToManage 对象本身是什么样,我们调用的始终是正确、安全的 hasOwnProperty 方法。这在编写高质量的库或框架代码时是一个非常重要的最佳实践。
typeof 和 keyof typeof
typeof (在类型上下文中使用)
功能:获取一个变量或对象的类型。它允许我们基于已存在的 JavaScript 代码(值)来 创建 TypeScript 类型。(从变量提取类型,函数返回值类型推断)
const person = { name: "Alice", age: 30 };// typeof person 的结果是类型:type PersonType = typeof person; //相对于type PersonType = { name: string, age: number }
keyof(直接操作类型)功能:获取一个类型的所有键(key),并创建一个由这些键组成的联合类型 (Union Type)
(我有个类型,想知道它有哪些属性)
type PersonType = { name: string, age: number };// keyof PersonType 的结果是类型:type PersonKeys = keyof PersonType; //相对于 type PersonKeys="name" | "age"
keyof typeof ( 通过值获取类型再获取键名)
则会进一步获取这个类型的所有键,形成一个联合类型(我有个变量,想知道它有哪些属性)
// 先有变量(值) const person = {name: "Alice",age: 30,email: "alice@example.com" };// 通过变量获取类型,再获取键名 type PersonKeys = keyof typeof person; // 结果: "name" | "age" | "email"