import { defineStore } from 'pinia' import { ref, computed } from 'vue' /** * 模块标签栏跨组件状态。 * * 用于在模块详情页的 TabsList 因滚动消失时, * 在顶部 TitleBar 中显示一组浮动切换按钮。 * * 工作流程: * 1. 模块(如 DownloaderModule)onMounted 时调用 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([]) /** 当前活跃标签值 */ const activeTab = ref('') /** 是否显示浮动切换器(TabsList 滚出可视区时为 true) */ const floatingVisible = ref(false) /** 待跳转 tab(搜索导航设置):{ moduleId, tab },模块挂载/已挂载时消费 */ const pendingTab = ref<{ moduleId: string; tab: string } | null>(null) /** * 各模块上次停留的 tab(内存记忆)。 * * 模块被切走时组件卸载、`activeTab` 随之丢失,再切回来总是回到第一个 tab—— * 对「曲库 / 发现音乐 / 设置」这种三级结构的模块尤其别扭。 * 只做内存记忆(不落盘):tab 集合可能随版本变化,跨版本恢复旧值反而容易出错。 */ const lastTabByModule = ref>({}) /** 当前已注册的模块 id(unregister 时据此保存) */ 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(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 } })