376 lines
10 KiB
Plaintext
376 lines
10 KiB
Plaintext
/**
|
||
* 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 {
|
||
// 即使不新增 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<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
|
||
}
|
||
}
|
||
}
|