/** * Admin 导航状态管理 * 管理路由切换、菜单选中、标签页等状态 */ import { ref, computed } from 'vue' import type { RouteRecord } from '@/layouts/admin/router/adminRoutes.uts' import { findRouteById, findRouteByPath, buildDefaultTabs, getTopMenus } from '@/layouts/admin/router/adminRoutes.uts' import { addView, activeFullPath, visitedViews } from './tagsViewStore.uts' import { hasAdminModuleAccess } from '@/layouts/admin/utils/role.uts' // ============================================ // Tabs 持久化 keys(使用 sessionStorage,与 token 策略一致) // sessionStorage 在同一标签页 F5 刷新后保留,关闭标签页后自动清除 // ============================================ const TAB_STORAGE_KEY = 'admin_opened_tabs' const ACTIVE_ROUTE_KEY = 'admin_active_route' const LAST_SUB_STORAGE_KEY = 'admin_last_sub_by_menu' /** 将当前 tabs / activeRouteId / lastSubIdByMenu 写入 sessionStorage */ function persistNavState(): void { // #ifdef H5 try { sessionStorage.setItem(TAB_STORAGE_KEY, JSON.stringify(tabs.value)) sessionStorage.setItem(ACTIVE_ROUTE_KEY, activeRouteId.value) sessionStorage.setItem(LAST_SUB_STORAGE_KEY, JSON.stringify(lastSubIdByMenu.value)) } catch (_) {} // #endif } /** * 标签页类型 */ export type TabItem = { id: string title: string path: string isAffix: boolean // 是否固定(不可关闭) } // ============================================ // 状态定义 // ============================================ /** 当前选中的一级菜单ID */ export const activeTopMenuId = ref('home') /** 当前激活的路由ID */ export const activeRouteId = ref('home_index') /** 记录每个一级模块上一次访问的二级路由ID (CRMEB 体验增强) */ export const lastSubIdByMenu = ref>({}) /** 标记是否由用户手动关闭了 SubSider (移动端) */ export const isManualClosed = ref(false) /** 打开的标签页列表 */ export const tabs = ref([]) /** 是否折叠主侧边栏 (CRMEB: 70px) */ export const isMainAsideCollapsed = ref(false) /** 是否显示二级侧边栏状态控制 */ export const showSubSider = ref(true) /** 屏幕宽度 */ export const windowWidth = ref(1024) /** 布局模式:desktop | tablet | mobile */ export const layoutMode = computed(() => { if (windowWidth.value < 768) return 'mobile' if (windowWidth.value < 1200) return 'tablet' return 'desktop' }) /** 是否为移动端简易判断 */ export const isMobile = computed(() => layoutMode.value === 'mobile') /** 移动端开关 */ export const isMobileMenuOpen = ref(false) /** 遮罩层开关 (用于 tablet 的 subSider overlay 和 mobile 的 aside overlay) */ export const isOverlayVisible = ref(false) // ============================================ // Actions // ============================================ /** * 打开路由(核心方法) * @param routeId 路由ID * @param addTab 是否添加到标签页 */ export function openRoute(routeId: string, addTab: boolean = true): void { const route = findRouteById(routeId) if (!route) { console.warn(`[AdminNav] Route not found: ${routeId}`) return } // 基于 role 的页面访问拦截 // route.parentId 对应上方 topMenus 的 id。这里校验是否有权限 const moduleId = route.parentId ? route.parentId : route.id.split('_')[0] if (!hasAdminModuleAccess(moduleId)) { uni.showToast({ title: '您没有权限访问该模块', icon: 'none' }) console.warn(`[AdminNav] Access denied for role to module: ${moduleId}`) // 回退到首页 if (routeId !== 'home_index') { openRoute('home_index', addTab) } return } // 更新当前路由 activeRouteId.value = routeId // 更新一级菜单选中态 if (route.parentId) { activeTopMenuId.value = route.parentId // 记录该模块最后访问的子路由 lastSubIdByMenu.value[route.parentId] = routeId } else { // 首页等顶级路由 activeTopMenuId.value = routeId.split('_')[0] } // 添加到标签页 if (addTab) { addTabItem(route) } else { // 即使不新增 tab,activeRouteId 变化了也要持久化 persistNavState() } } /** * 通过路径打开路由 */ export function openRouteByPath(path: string): void { const route = findRouteByPath(path) if (route) { openRoute(route.id) } } /** * 添加标签页 */ function addTabItem(route: RouteRecord): void { const existingTab = tabs.value.find(t => t.id === route.id) if (!existingTab) { tabs.value.push({ id: route.id, title: route.title, path: route.path, isAffix: route.isAffix || false }) } // 更新新版 TagsViewStore addView(route, route.path) activeFullPath.value = route.path // 持久化到 sessionStorage persistNavState() } /** * 关闭标签页 * @param tabId 标签ID */ export function closeTab(tabId: string): void { const index = tabs.value.findIndex(t => t.id === tabId) if (index === -1) return const tab = tabs.value[index] // 固定标签不可关闭 if (tab.isAffix) { console.warn(`[AdminNav] Cannot close fixed tab: ${tabId}`) return } // 如果关闭的是当前激活标签,需要切换到其他标签 if (activeRouteId.value === tabId) { // 优先切换到右侧标签,否则切换到左侧 const nextTab = tabs.value[index + 1] || tabs.value[index - 1] if (nextTab) { openRoute(nextTab.id, false) } } tabs.value.splice(index, 1) // 持久化 persistNavState() } /** * 关闭其他标签页 * @param keepTabId 保留的标签ID */ export function closeOtherTabs(keepTabId: string): void { tabs.value = tabs.value.filter(t => t.isAffix || t.id === keepTabId) // 如果当前激活的标签被关闭了,切换到保留的标签 const stillExists = tabs.value.find(t => t.id === activeRouteId.value) if (!stillExists) { openRoute(keepTabId, false) } // 持久化 persistNavState() } /** * 关闭所有标签页(保留固定标签) */ export function closeAllTabs(): void { tabs.value = tabs.value.filter(t => t.isAffix) // 切换到首页 const homeTab = tabs.value.find(t => t.isAffix) if (homeTab) { openRoute(homeTab.id, false) } // 持久化 persistNavState() } /** * 切换主侧边栏折叠状态 */ export function toggleMainAsideCollapse(): void { isMainAsideCollapsed.value = !isMainAsideCollapsed.value } /** * 切换二级侧边栏显示状态 (Desktop 模式) */ export function toggleSubSider(): void { showSubSider.value = !showSubSider.value } /** * 初始化导航状态(首次进入 / 恢复失败时的兜底) * 在 AdminLayout 组件 onMounted 时调用 */ export function initNavState(): void { // 初始化默认标签页 const defaultTabs = buildDefaultTabs() tabs.value = defaultTabs.map(r => ({ id: r.id, title: r.title, path: r.path, isAffix: r.isAffix || false })) // 初始化 TagsViewStore defaultTabs.forEach(r => { addView(r, r.path) }) // 打开首页 openRoute('home_index', false) } /** * 从 sessionStorage 恢复导航状态(F5刷新后调用) * - 校验每个缓存 tab 的路由是否仍然存在 * - 校验是否仍有权限 * - 过滤无效项后重建 tabs * - 恢复 activeRouteId 和菜单高亮 * @returns true=恢复成功,false=无有效缓存或恢复失败(调用方应走 initNavState 兜底) */ export function restoreNavState(): boolean { // #ifdef H5 try { const rawTabs = sessionStorage.getItem(TAB_STORAGE_KEY) if (!rawTabs) return false const savedTabs = JSON.parse(rawTabs) as TabItem[] if (!Array.isArray(savedTabs) || savedTabs.length === 0) return false // 校验并过滤:路由必须存在且有权限 const validTabs = savedTabs.filter(tab => { const route = findRouteById(tab.id) if (!route) return false // 路由已不存在(文件删除或路由表变更) const moduleId = route.parentId ?? tab.id.split('_')[0] return hasAdminModuleAccess(moduleId) // 检查权限 }) // 首页 tab 始终保留(防止全部被过滤后无处可去) const homeRoute = findRouteById('home_index') if (homeRoute && !validTabs.some(t => t.id === 'home_index')) { validTabs.unshift({ id: homeRoute.id, title: homeRoute.title, path: homeRoute.path, isAffix: homeRoute.isAffix || false }) } if (validTabs.length === 0) return false // 重建 tabs tabs.value = validTabs validTabs.forEach(tab => { const route = findRouteById(tab.id) if (route) addView(route, route.path) }) // 恢复 lastSubIdByMenu try { const rawLast = sessionStorage.getItem(LAST_SUB_STORAGE_KEY) if (rawLast) { lastSubIdByMenu.value = JSON.parse(rawLast) as Record } } catch (_) {} // 恢复 activeRouteId const savedActive = sessionStorage.getItem(ACTIVE_ROUTE_KEY) const activeIsValid = savedActive != null && findRouteById(savedActive) != null && validTabs.some(t => t.id === savedActive) const targetId = activeIsValid ? savedActive! : validTabs[0].id activeRouteId.value = targetId activeFullPath.value = findRouteById(targetId)?.path ?? '' const activeRoute = findRouteById(targetId) if (activeRoute?.parentId) { activeTopMenuId.value = activeRoute.parentId } else { activeTopMenuId.value = targetId.split('_')[0] } console.log('[AdminNav] 已从缓存恢复导航状态, tabs:', validTabs.length, 'active:', targetId) return true } catch (e) { console.warn('[AdminNav] 恢复导航状态失败,将走默认初始化:', e) return false } // #endif return false } /** * 根据 currentPage 同步状态 * 用于页面组件传入 currentPage prop 时的状态同步 */ export function syncFromCurrentPage(currentPage: string): void { if (!currentPage) return // 可能是路由ID或路径 const route = findRouteById(currentPage) || findRouteByPath(currentPage) if (route) { activeRouteId.value = route.id // 更新一级菜单 if (route.parentId) { activeTopMenuId.value = route.parentId } } }