117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
import { nextTick, onMounted, onUnmounted, ref, watch, type Ref } from 'vue'
|
||
import { useModuleTabsStore, type ModuleTab } from '@/stores/moduleTabsStore'
|
||
|
||
/**
|
||
* 模块标签栏通用 composable。
|
||
*
|
||
* 让任何模块的 TabsList 在滚动被 TitleBar 遮挡时,
|
||
* 自动在 TitleBar 中显示一组浮动切换按钮。
|
||
*
|
||
* ## 使用方式
|
||
*
|
||
* ```ts
|
||
* // 模块 <script setup> 顶部
|
||
* const activeTab = ref('overview')
|
||
* const tabsListRef = useModuleTabs('proxy', activeTab, [
|
||
* { value: 'overview', label: '概览' },
|
||
* { value: 'settings', label: '设置' }
|
||
* ])
|
||
* ```
|
||
*
|
||
* 第一个参数为模块 id:搜索导航跳转时,模块挂载后会自动
|
||
* 消费 moduleTabsStore 中对应的待跳转 tab(pendingTab)。
|
||
*
|
||
* ```vue
|
||
* <!-- 模板中给 TabsList 包一层带 ref 的 div -->
|
||
* <div ref="tabsListRef">
|
||
* <TabsList>...</TabsList>
|
||
* </div>
|
||
* ```
|
||
*
|
||
* ## 工作原理
|
||
* 1. onMounted 时注册标签到 moduleTabsStore,TitleBar 据此渲染浮动切换器
|
||
* 2. 用 IntersectionObserver 监听 TabsList 可见性(rootMargin 裁剪 TitleBar 高度)
|
||
* 3. 双向 watch 同步本地 activeTab 与 store.activeTab
|
||
* 4. 消费搜索导航的待跳转 tab(模块尚未挂载的场景)
|
||
* 5. onUnmounted 时清理 observer 并注销标签
|
||
*
|
||
* ## 约束
|
||
* - TitleBar 高度固定为 40px (h-10),composable 内部已用 44px 裁剪(含缓冲)
|
||
* - 一个模块同一时间只能注册一组标签(store 是单例)
|
||
* - 模块卸载时务必让 composable 的 onUnmounted 执行(已自动处理,无需手动调用)
|
||
*/
|
||
export function useModuleTabs(
|
||
moduleId: string,
|
||
activeTab: Ref<string>,
|
||
tabs: ModuleTab[]
|
||
): Ref<HTMLElement | null> {
|
||
const tabsStore = useModuleTabsStore()
|
||
const tabsListRef = ref<HTMLElement | null>(null)
|
||
|
||
let observer: IntersectionObserver | null = null
|
||
|
||
/** 应用待跳转 tab(若属于当前模块的 tab 列表) */
|
||
const applyPendingTab = () => {
|
||
const pending = tabsStore.consumePendingTab(moduleId)
|
||
if (pending && tabs.some(t => t.value === pending)) {
|
||
activeTab.value = pending
|
||
tabsStore.setActiveTab(pending)
|
||
}
|
||
}
|
||
|
||
const setupObserver = () => {
|
||
const el = tabsListRef.value
|
||
if (!el || observer) return
|
||
observer = new IntersectionObserver(
|
||
(entries) => {
|
||
for (const entry of entries) {
|
||
tabsStore.setFloatingVisible(!entry.isIntersecting)
|
||
}
|
||
},
|
||
{
|
||
// root=null 表示视口;顶部裁剪 44px(TitleBar 高度 40px + 4px 缓冲)
|
||
rootMargin: '-44px 0px 0px 0px',
|
||
threshold: 0
|
||
}
|
||
)
|
||
observer.observe(el)
|
||
}
|
||
|
||
// 本地 activeTab → store(用户点击模块内 TabsTrigger 时同步)
|
||
watch(activeTab, (val) => {
|
||
tabsStore.setActiveTab(val)
|
||
})
|
||
|
||
// store activeTab → 本地(用户点击 TitleBar 浮动切换器时同步)
|
||
watch(() => tabsStore.activeTab, (val) => {
|
||
if (val && val !== activeTab.value) {
|
||
activeTab.value = val
|
||
}
|
||
})
|
||
|
||
// 模块已挂载时(搜索结果选中同一模块),pendingTab 变化 → 直接切换 tab
|
||
watch(() => tabsStore.pendingTab, (p) => {
|
||
if (p?.moduleId === moduleId) {
|
||
applyPendingTab()
|
||
}
|
||
})
|
||
|
||
onMounted(async () => {
|
||
tabsStore.registerTabs(tabs, activeTab.value)
|
||
await nextTick()
|
||
setupObserver()
|
||
// 搜索导航跳转:模块刚挂载,消费待跳转 tab
|
||
applyPendingTab()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (observer) {
|
||
observer.disconnect()
|
||
observer = null
|
||
}
|
||
tabsStore.unregisterTabs()
|
||
})
|
||
|
||
return tabsListRef
|
||
}
|