Files
medical-mall/layouts/admin/store/adminNavStore.uts

376 lines
10 KiB
Plaintext
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.
/**
* 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<string>('home')
/** 当前激活的路由ID */
export const activeRouteId = ref<string>('home_index')
/** 记录每个一级模块上一次访问的二级路由ID (CRMEB 体验增强) */
export const lastSubIdByMenu = ref<Record<string, string>>({})
/** 标记是否由用户手动关闭了 SubSider (移动端) */
export const isManualClosed = ref<boolean>(false)
/** 打开的标签页列表 */
export const tabs = ref<TabItem[]>([])
/** 是否折叠主侧边栏 (CRMEB: 70px) */
export const isMainAsideCollapsed = ref<boolean>(false)
/** 是否显示二级侧边栏状态控制 */
export const showSubSider = ref<boolean>(true)
/** 屏幕宽度 */
export const windowWidth = ref<number>(1024)
/** 布局模式desktop | tablet | mobile */
export const layoutMode = computed<string>(() => {
if (windowWidth.value < 768) return 'mobile'
if (windowWidth.value < 1200) return 'tablet'
return 'desktop'
})
/** 是否为移动端简易判断 */
export const isMobile = computed<boolean>(() => layoutMode.value === 'mobile')
/** 移动端开关 */
export const isMobileMenuOpen = ref<boolean>(false)
/** 遮罩层开关 (用于 tablet 的 subSider overlay 和 mobile 的 aside overlay) */
export const isOverlayVisible = ref<boolean>(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 {
// 即使不新增 tabactiveRouteId 变化了也要持久化
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<string, string>
}
} 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
}
}
}