diff --git a/layouts/admin/AdminLayout.uvue b/layouts/admin/AdminLayout.uvue index da188f0e..fa7f4908 100644 --- a/layouts/admin/AdminLayout.uvue +++ b/layouts/admin/AdminLayout.uvue @@ -116,7 +116,8 @@ import { closeAllTabs, toggleMainAsideCollapse as storeToggleCollapse, toggleSubSider as storeToggleSubSider, - initNavState + initNavState, + restoreNavState } from '@/layouts/admin/store/adminNavStore.uts' import type { TabItem } from '@/layouts/admin/store/adminNavStore.uts' @@ -418,10 +419,18 @@ onMounted(async () => { // #endif isAuthReady.value = true - initNavState() - if (props.currentPage != '') { - openRoute(props.currentPage as string) + + // 第二段:尝试从 sessionStorage 恢复上次打开的页签状态 + // restoreNavState 会校验路由有效性和权限,过滤无效项后重建 tabs + const restored = restoreNavState() + if (!restored) { + // 首次打开 / 缓存为空 / 恢复失败 → 走默认初始化(只有首页tab) + initNavState() + if (props.currentPage != '') { + openRoute(props.currentPage as string) + } } + // restored=true 时,activeRouteId 已由 restoreNavState 正确设置,无需额外跳转 // 初始化窗口宽度 windowWidth.value = uni.getWindowInfo().windowWidth diff --git a/layouts/admin/store/adminNavStore.uts b/layouts/admin/store/adminNavStore.uts index dbaf6914..34f7d2dc 100644 --- a/layouts/admin/store/adminNavStore.uts +++ b/layouts/admin/store/adminNavStore.uts @@ -14,6 +14,25 @@ import { 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 +} + /** * 标签页类型 */ @@ -120,6 +139,9 @@ export function openRoute(routeId: string, addTab: boolean = true): void { // 添加到标签页 if (addTab) { addTabItem(route) + } else { + // 即使不新增 tab,activeRouteId 变化了也要持久化 + persistNavState() } } @@ -150,6 +172,9 @@ function addTabItem(route: RouteRecord): void { // 更新新版 TagsViewStore addView(route, route.path) activeFullPath.value = route.path + + // 持久化到 sessionStorage + persistNavState() } /** @@ -178,6 +203,8 @@ export function closeTab(tabId: string): void { } tabs.value.splice(index, 1) + // 持久化 + persistNavState() } /** @@ -192,6 +219,8 @@ export function closeOtherTabs(keepTabId: string): void { if (!stillExists) { openRoute(keepTabId, false) } + // 持久化 + persistNavState() } /** @@ -205,6 +234,8 @@ export function closeAllTabs(): void { if (homeTab) { openRoute(homeTab.id, false) } + // 持久化 + persistNavState() } /** @@ -222,7 +253,7 @@ export function toggleSubSider(): void { } /** - * 初始化导航状态 + * 初始化导航状态(首次进入 / 恢复失败时的兜底) * 在 AdminLayout 组件 onMounted 时调用 */ export function initNavState(): void { @@ -244,6 +275,86 @@ export function initNavState(): void { 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 时的状态同步 diff --git a/layouts/admin/utils/adminAuth.uts b/layouts/admin/utils/adminAuth.uts index 6d1c0f0d..0e839fd0 100644 --- a/layouts/admin/utils/adminAuth.uts +++ b/layouts/admin/utils/adminAuth.uts @@ -1,6 +1,6 @@ import { state, getCurrentUser } from '@/utils/store.uts' import { clearAdminRoleCache, refreshAdminRole } from './role.uts' -import supa from '@/components/supadb/aksupainstance.uts' +import supa, { supaReady } from '@/components/supadb/aksupainstance.uts' let __isHandlingExpired = false @@ -51,6 +51,11 @@ export function handleSessionExpired(reason?: string) { */ export async function ensureAdminSession(): Promise { try { + // ① 等待 hydrateSessionFromStorage() 完成(从storage恢复token→异步网络请求) + // 必须在 getSession() 之前 await,否则刷新后 this.session 仍为 null, + // 会被误判为未登录并触发 handleSessionExpired。 + try { await supaReady } catch (_) {} + const sessionInfo = supa.getSession() if (sessionInfo.session == null) { console.warn('[AdminAuth] 没有发现凭证,要求重新登录')