在開發復雜的單頁應用(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"