diff --git a/src/layout/MainLayout.vue b/src/layout/MainLayout.vue index e2777ee..3afde0e 100644 --- a/src/layout/MainLayout.vue +++ b/src/layout/MainLayout.vue @@ -23,7 +23,8 @@ + :class="{ 'is-hidden': !tabBarStore.isShowing, 'skip-transition': tabBarStore.shouldSkipTransition }" class="tab-bar glass-white" v-model="tabVal" shape="round" @@ -261,6 +262,11 @@ const contentHeight = computed(() => { transform: translateY(calc(100% + 1rem)); } + &.skip-transition { + // 配合 tabBarStore.shouldSkipTransition:仅本次导航禁用过渡 + transition: none; + } + .item { margin: 0; padding: 0; diff --git a/src/router/index.ts b/src/router/index.ts index 64e6bed..8912c75 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -15,9 +15,11 @@ import ServerPerformanceDetail from "@/pages/dashboard/ServerDashboard/ServerPer import ServerPartitionsDetail from "@/pages/dashboard/ServerDashboard/ServerPartitionsDetail.vue"; import DockerContainerDetail from "@/pages/dashboard/DockerDashboard/DockerContainerDetail.vue"; import { + hasProgrammaticBackNavigation, clearProgrammaticBackNavigation, markProgrammaticBackNavigation } from "@/utils/backNavigationSignal"; +import { useTabBarStore } from "@/store/tabBarStore"; const router = createRouter({ history: createWebHistory("/"), @@ -186,6 +188,29 @@ const router = createRouter({ const rawBack = router.back.bind(router); const rawGo = router.go.bind(router); +const isIOS = /iP(ad|hone|od)/i.test(window.navigator.userAgent); +// 与 MainLayout 的 tabbar 过渡时长保持一致,额外预留一点缓冲时间 +const TAB_BAR_TRANSITION_MS = 520; +const TAB_BAR_SKIP_EXTRA_MS = 180; +const depthCache = new Map(); + +function calcDepth(sourceRoute: RouteLocationNormalized): number { + if (!sourceRoute.meta.dynamicDepth) { + return Number(sourceRoute.meta.depth ?? 0); + } + + const fullPath = sourceRoute.fullPath; + const cachedDepth = depthCache.get(fullPath); + if (cachedDepth !== undefined) { + return cachedDepth; + } + + const baseDepth = Number(sourceRoute.meta.baseDepth ?? 0); + const pathSegments = fullPath.split("?")[0].split("/").filter((pathSegment) => pathSegment !== ""); + const routeDepth = baseDepth + (pathSegments.length - 1); + depthCache.set(fullPath, routeDepth); + return routeDepth; +} // 全局代理:所有代码触发的后退都自动标记为“主动返回”。 router.back = () => { @@ -201,8 +226,20 @@ router.go = (delta) => { rawGo(delta); }; -router.beforeEach((to: RouteLocationNormalized) => { +router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized) => { + const tabBarStore = useTabBarStore(); const settingStore = useSettingStore(); + const toDepth = calcDepth(to); + const fromDepth = calcDepth(from); + const isBackwardNavigation = toDepth < fromDepth; + // 代码主动调用 back/go(-1) 的返回,仍保留 tabbar 进入动画 + const isProgrammaticBack = hasProgrammaticBackNavigation(); + const isTabBarEntering = !from.meta.tabBarVisible && !!to.meta.tabBarVisible; + // 仅 iOS 手势返回(非代码触发)且无 tabbar -> 有 tabbar 时,禁用本次 tabbar 进入动画 + const shouldSkipTabBarEnterTransition = isIOS && isBackwardNavigation && !isProgrammaticBack && isTabBarEntering; + if (shouldSkipTabBarEnterTransition) { + tabBarStore.skipTransitionOnce(); + } if (to.meta.ignoreConnectCheck) { return true; @@ -221,6 +258,13 @@ router.beforeEach((to: RouteLocationNormalized) => { }); router.afterEach((to: RouteLocationNormalized) => { + const tabBarStore = useTabBarStore(); + if (tabBarStore.shouldSkipTransition) { + // 本次导航稳定后再恢复过渡,避免手势返回期间出现二次进入动画 + window.setTimeout(() => { + tabBarStore.restoreTransition(); + }, TAB_BAR_TRANSITION_MS + TAB_BAR_SKIP_EXTRA_MS); + } // 一次导航结束后清理信号,防止影响后续动画判定。 clearProgrammaticBackNavigation(); const globalUIStore = useGlobalUIStore(); diff --git a/src/store/tabBarStore.ts b/src/store/tabBarStore.ts index cfc0bc3..e13e716 100644 --- a/src/store/tabBarStore.ts +++ b/src/store/tabBarStore.ts @@ -2,10 +2,23 @@ import { defineStore } from "pinia"; export const useTabBarStore = defineStore("tab-bar", () => { const router = useRouter(); + // 仅用于“当前这一次”禁用 tabbar 过渡,导航结束后会恢复 + const shouldSkipTransition = ref(false); const isShowing = computed(() => !!router.currentRoute.value.meta.tabBarVisible); + function skipTransitionOnce(): void { + shouldSkipTransition.value = true; + } + + function restoreTransition(): void { + shouldSkipTransition.value = false; + } + return { - isShowing + isShowing, + shouldSkipTransition, + skipTransitionOnce, + restoreTransition }; });