import { useEffect, useRef, useState } from 'react' type Listener = (layers: Layer[]) => void type Layer = { id: number role?: string } /** * 管理层级(Layer)的单例 * * 类似堆栈的管理方式,先进去的 Layer 后销毁。最后 push 进去的 Layer 因为离视窗最近所以最先销毁 */ class StackStore { private listeners: Listener[] = [] public layers: Layer[] = [] listen(listener: Listener) { this.listeners.push(listener) return () => { this.listeners = this.listeners.filter(l => l !== listener) } } emit() { this.listeners.forEach(listener => listener(this.layers)) } push(id: number, role?: string) { this.layers.push({ id, role }) this.emit() } pop() { this.layers.pop() this.emit() } } const stackStore = new StackStore() let id = 0 /** * TODO: 可能要重构 * 获取 role 相同的分组里的 Mask 的 Level 数据 * * 使用此 hook 的组件在 visible 为 true 后会往 `StackStore` push 一层,为 false 的时候从 `StackStore` pop 一层 * * @param role 当前组件在 `role` 分组里所在的 Mask 层级数 */ export function useLayer(visible: boolean, role: string) { const [selfId] = useState(() => ++id) // 该层在所有层级中的 level,不受 role 影响,即使传了 role 参数 const [absLevel, setAbsLevel] = useState(0) // 组件当前在按 role 分组后的层里是哪层 // 比如按离视窗从远到近:Drawer(1) -> Modal(1) -> Drawer(2) -> Modal(2) const [roleLevel, setRoleLevel] = useState(0) // 离视窗最近的 level 在哪层 const [topmostRoleLevel, setTopmostRoleLevel] = useState(0) const prevZIndex = useRef(defaultZIndex) // false 的时候保留 zIndex,防止动画过程中出现交换层级的视觉效果 const zIndex = visible === true ? defaultZIndex + absLevel * increasingZIndexPerLevel : prevZIndex.current prevZIndex.current = zIndex useEffect(() => { const unlisten = stackStore.listen(layers => { const groupedLayersByRole = layers.filter(layer => role == null || layer.role === role) const index = groupedLayersByRole.findIndex(layer => layer.id === selfId) const absIndex = layers.findIndex(layer => layer.id === selfId) setRoleLevel(index + 1) setTopmostRoleLevel(groupedLayersByRole.length) setAbsLevel(absIndex + 1) }) return () => unlisten() }, [role, selfId]) const isOpenedRef = useRef(false) // watch visible 的变化 useEffect(() => { if (isOpenedRef.current === false && visible === true) { isOpenedRef.current = true stackStore.push(selfId, role) return } if (isOpenedRef.current === true && visible === false) { isOpenedRef.current = false stackStore.pop() } }, [role, selfId, visible]) // unmount clean effect // 如果是 { visible && } 等 visible 不会变化的场景, // 则不会触发上面的 effect 的,也就不会 pop layer useEffect(() => () => { if (isOpenedRef.current === true) { stackStore.pop() } }, []) return { id: selfId, absLevel, roleLevel, topmostRoleLevel, zIndex } } // @icecream-z-index-modal - 1 const defaultZIndex = 1100 - 1 // 项目超过 9 层会超过 @icecream-z-index-modal-upside const increasingZIndexPerLevel = 10