Files
Thing/src/stores/moduleTabsStore.ts
T
2026-09-15 17:17:16 +08:00

157 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
/**
* 模块标签栏跨组件状态。
*
* 用于在模块详情页的 TabsList 因滚动消失时,
* 在顶部 TitleBar 中显示一组浮动切换按钮。
*
* 工作流程:
* 1. 模块(如 DownloaderModuleonMounted 时调用 registerTabs(),传入标签定义和当前值
* 2. 模块用 IntersectionObserver 或 scroll 监听检测 TabsList 可见性,调用 setFloatingVisible()
* 3. 模块通过 watch 将本地 activeTab 同步到 store
* 4. TitleBar 读取 store 的 tabs / activeTab / floatingVisible 渲染浮动切换器
* 5. 用户点击浮动切换器时调用 setActiveTab(),模块监听 store.activeTab 变化更新本地值
* 6. 模块 onUnmounted 时调用 unregisterTabs() 清理状态
*/
export interface ModuleTab {
value: string
label: string
}
export const useModuleTabsStore = defineStore('moduleTabs', () => {
/** 当前模块注册的标签列表(空表示无模块注册,TitleBar 不渲染) */
const tabs = ref<ModuleTab[]>([])
/** 当前活跃标签值 */
const activeTab = ref<string>('')
/** 是否显示浮动切换器(TabsList 滚出可视区时为 true */
const floatingVisible = ref<boolean>(false)
/** 待跳转 tab(搜索导航设置):{ moduleId, tab },模块挂载/已挂载时消费 */
const pendingTab = ref<{ moduleId: string; tab: string } | null>(null)
/**
* 各模块上次停留的 tab(内存记忆)。
*
* 模块被切走时组件卸载、`activeTab` 随之丢失,再切回来总是回到第一个 tab——
* 对「曲库 / 发现音乐 / 设置」这种三级结构的模块尤其别扭。
* 只做内存记忆(不落盘):tab 集合可能随版本变化,跨版本恢复旧值反而容易出错。
*/
const lastTabByModule = ref<Record<string, string>>({})
/** 当前已注册的模块 idunregister 时据此保存) */
let currentModuleId = ''
/** 设置待跳转 tab(搜索结果点击时调用) */
const setPendingTab = (moduleId: string, tab: string) => {
pendingTab.value = { moduleId, tab }
}
/**
* 消费指定模块的待跳转 tab(返回 tab 值并清除)。
* 仅在 moduleId 匹配时消费,避免误切当前已挂载模块的 tab。
*/
const consumePendingTab = (moduleId: string): string | null => {
if (pendingTab.value && pendingTab.value.moduleId === moduleId) {
const tab = pendingTab.value.tab
pendingTab.value = null
return tab
}
return null
}
/** 当前模块注册的保存处理函数(null 表示无保存按钮,如自动保存模块) */
const saveHandler = ref<(() => unknown) | null>(null)
/** 保存中状态(驱动按钮 disabled + loading 图标) */
const saving = ref<boolean>(false)
/** 是否显示保存按钮(当前在设置 tab 且注册了保存处理函数) */
const saveVisible = computed(() => saveHandler.value !== null && activeTab.value === 'settings')
/**
* 模块注册标签(onMounted 时调用)。
* 返回该模块**应当使用**的 tab:优先复用上次停留的(且在当前 tab 集合内),
* 否则用传入的初始值。调用方若拿到不同值,需同步自己的 activeTab。
*/
const registerTabs = (moduleId: string, tabList: ModuleTab[], current: string): string => {
currentModuleId = moduleId
tabs.value = tabList
const remembered = lastTabByModule.value[moduleId]
const restored =
remembered && tabList.some((t) => t.value === remembered) ? remembered : current
activeTab.value = restored
floatingVisible.value = false
return restored
}
/**
* 模块注销标签(onUnmounted 时调用)。
* `moduleId` 缺省时用当前注册的模块(保留旧调用点的语义)。
*/
const unregisterTabs = (moduleId?: string) => {
const id = moduleId ?? currentModuleId
if (id && activeTab.value) lastTabByModule.value[id] = activeTab.value
currentModuleId = ''
tabs.value = []
activeTab.value = ''
floatingVisible.value = false
saveHandler.value = null
saving.value = false
}
/** 设置浮动切换器可见性 */
const setFloatingVisible = (visible: boolean) => {
// 仅在有注册标签时才允许显示
if (!visible) {
floatingVisible.value = false
return
}
if (tabs.value.length > 0) {
floatingVisible.value = true
}
}
/**
* 设置活跃标签(双向同步用)。
* - 模块本地 activeTab 变化时调用此方法同步到 store
* - TitleBar 点击时也调用此方法,模块通过 watch 感知变化
*/
const setActiveTab = (value: string) => {
activeTab.value = value
}
/** 模块注册保存处理函数(setup 中定义保存方法后调用) */
const registerSave = (handler: (() => unknown) | null) => {
saveHandler.value = handler
saving.value = false
}
/** 执行保存(TitleBar 按钮点击时调用) */
const runSave = async () => {
if (!saveHandler.value || saving.value) return
saving.value = true
try {
await saveHandler.value()
} finally {
saving.value = false
}
}
return {
tabs,
activeTab,
floatingVisible,
pendingTab,
saveHandler,
saving,
saveVisible,
registerTabs,
unregisterTabs,
setFloatingVisible,
setActiveTab,
setPendingTab,
consumePendingTab,
registerSave,
runSave
}
})