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
};
});