Compare commits
4 Commits
631122c79b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e192a90e1 | ||
|
|
6f067477f0 | ||
|
|
11c1199449 | ||
|
|
42efa2b370 |
@@ -14,6 +14,7 @@
|
|||||||
import type { RouteLocationNormalized } from "vue-router";
|
import type { RouteLocationNormalized } from "vue-router";
|
||||||
import { viewDepthKey } from "vue-router";
|
import { viewDepthKey } from "vue-router";
|
||||||
import { useGlobalUIStore } from "@/store/globalUIStore";
|
import { useGlobalUIStore } from "@/store/globalUIStore";
|
||||||
|
import { hasProgrammaticBackNavigation } from "@/utils/backNavigationSignal";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "PageTransition"
|
name: "PageTransition"
|
||||||
@@ -26,6 +27,8 @@ const viewDepth = inject(viewDepthKey, 0);
|
|||||||
|
|
||||||
const transitionName = ref("");
|
const transitionName = ref("");
|
||||||
const hasTransition = ref(false);
|
const hasTransition = ref(false);
|
||||||
|
// iOS 左滑返回由系统接管,页面侧不应叠加离场动画。
|
||||||
|
const isIOS = /iP(ad|hone|od)/i.test(window.navigator.userAgent);
|
||||||
const pageBackground = computed(() => globalUIStore.bodyBackground);
|
const pageBackground = computed(() => globalUIStore.bodyBackground);
|
||||||
const currentDepth = computed(() => Number(unref(viewDepth)));
|
const currentDepth = computed(() => Number(unref(viewDepth)));
|
||||||
const pageKey = computed(() => {
|
const pageKey = computed(() => {
|
||||||
@@ -72,6 +75,17 @@ const unregisterGuard = router.beforeEach((to, from) => {
|
|||||||
|
|
||||||
const toDepth = calcDepth(to);
|
const toDepth = calcDepth(to);
|
||||||
const fromDepth = calcDepth(from);
|
const fromDepth = calcDepth(from);
|
||||||
|
const isBackwardNavigation = toDepth < fromDepth;
|
||||||
|
// 主动触发的返回(如导航栏返回)保留动画。
|
||||||
|
const isProgrammaticBack = hasProgrammaticBackNavigation();
|
||||||
|
// 仅在 iOS 系统返回手势下关闭动画。
|
||||||
|
const shouldDisableTransition = isIOS && isBackwardNavigation && !isProgrammaticBack;
|
||||||
|
|
||||||
|
if (shouldDisableTransition) {
|
||||||
|
transitionName.value = "";
|
||||||
|
hasTransition.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (fromDepth < toDepth) {
|
if (fromDepth < toDepth) {
|
||||||
transitionName.value = "push-left";
|
transitionName.value = "push-left";
|
||||||
@@ -102,8 +116,8 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
.pages {
|
.pages {
|
||||||
|
|||||||
@@ -22,8 +22,9 @@
|
|||||||
<div :class="{ 'is-fixed-height': isContentFixedHeight }" class="router-view">
|
<div :class="{ 'is-fixed-height': isContentFixedHeight }" class="router-view">
|
||||||
<page-transition />
|
<page-transition />
|
||||||
</div>
|
</div>
|
||||||
|
<!-- iOS 手势返回场景下临时关闭 tabbar 过渡,避免系统回退与页面动画叠加 -->
|
||||||
<t-tab-bar
|
<t-tab-bar
|
||||||
:class="{ 'is-hidden': !tabBarStore.isShowing }"
|
:class="{ 'is-hidden': !tabBarStore.isShowing, 'skip-transition': tabBarStore.shouldSkipTransition }"
|
||||||
class="tab-bar glass-white"
|
class="tab-bar glass-white"
|
||||||
v-model="tabVal"
|
v-model="tabVal"
|
||||||
shape="round"
|
shape="round"
|
||||||
@@ -129,33 +130,29 @@ function resolveTabValue(path: string): string {
|
|||||||
if (path === "/" || path.startsWith("/files/")) {
|
if (path === "/" || path.startsWith("/files/")) {
|
||||||
return "/";
|
return "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(() => route.path, (newPath: string) => {
|
||||||
() => route.path,
|
|
||||||
(newPath: string) => {
|
|
||||||
tabVal.value = resolveTabValue(newPath);
|
tabVal.value = resolveTabValue(newPath);
|
||||||
},
|
}, { immediate: true });
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function onChangeTab(value: string): void {
|
async function onChangeTab(value: string): Promise<void> {
|
||||||
if (value === "__music__") {
|
if (value === "__music__") {
|
||||||
musicPlayerStore.setPopupVisible(true);
|
musicPlayerStore.setPopupVisible(true);
|
||||||
tabVal.value = resolveTabValue(route.path);
|
tabVal.value = resolveTabValue(route.path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (resolveTabValue(route.path) === value) {
|
||||||
router.push(value);
|
return;
|
||||||
|
}
|
||||||
|
await router.replace(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTabBarPaddingNeeded = computed(() => {
|
const isTabBarPaddingNeeded = computed(() => {
|
||||||
if (!tabBarStore.isShowing) {
|
if (!tabBarStore.isShowing) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return route.meta.tabBarPadding !== false;
|
return route.meta.tabBarPadding !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,7 +160,6 @@ const tabBarPadding = computed(() => {
|
|||||||
if (isTabBarPaddingNeeded.value) {
|
if (isTabBarPaddingNeeded.value) {
|
||||||
return "6rem";
|
return "6rem";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "0rem";
|
return "0rem";
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -184,6 +180,7 @@ const contentHeight = computed(() => {
|
|||||||
.main-layout {
|
.main-layout {
|
||||||
--app-nav-offset: v-bind(topPadding);
|
--app-nav-offset: v-bind(topPadding);
|
||||||
--safe-top: env(safe-area-inset-top, 0px);
|
--safe-top: env(safe-area-inset-top, 0px);
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -192,6 +189,8 @@ const contentHeight = computed(() => {
|
|||||||
transition: padding-bottom .24s ease;
|
transition: padding-bottom .24s ease;
|
||||||
|
|
||||||
.nav-bar {
|
.nav-bar {
|
||||||
|
--td-navbar-padding-top: var(--safe-top);
|
||||||
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -199,11 +198,8 @@ const contentHeight = computed(() => {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
:deep(.t-navbar) {
|
:deep(.t-navbar) {
|
||||||
z-index: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-navbar__content) {
|
&__content {
|
||||||
color: var(--tui-black, #000);
|
color: var(--tui-black, #000);
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||||
background: rgba(250, 250, 250, .8);
|
background: rgba(250, 250, 250, .8);
|
||||||
@@ -211,6 +207,13 @@ const contentHeight = computed(() => {
|
|||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__left,
|
||||||
|
&__right {
|
||||||
|
top: var(----td-navbar-padding-top);
|
||||||
|
height: var(--td-navbar-height, 48px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.nav-extra {
|
.nav-extra {
|
||||||
gap: .35rem;
|
gap: .35rem;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -237,9 +240,9 @@ const contentHeight = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-bar {
|
.tab-bar {
|
||||||
bottom: 1rem;
|
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -259,6 +262,11 @@ const contentHeight = computed(() => {
|
|||||||
transform: translateY(calc(100% + 1rem));
|
transform: translateY(calc(100% + 1rem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.skip-transition {
|
||||||
|
// 配合 tabBarStore.shouldSkipTransition:仅本次导航禁用过渡
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -731,6 +731,12 @@ function clearAutoRefreshTimer(): void {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
|
:deep(svg),
|
||||||
|
:deep(canvas) {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
|||||||
@@ -772,6 +772,12 @@ onMounted(restartAutoRefresh);
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
|
:deep(svg),
|
||||||
|
:deep(canvas) {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
|||||||
@@ -615,6 +615,12 @@ function clearAutoRefreshTimer(): void {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
|
:deep(svg),
|
||||||
|
:deep(canvas) {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ function saveConnect(): void {
|
|||||||
theme: "success",
|
theme: "success",
|
||||||
message: "连接配置已保存"
|
message: "连接配置已保存"
|
||||||
});
|
});
|
||||||
|
router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import ServerDetail from "@/pages/dashboard/ServerDashboard/ServerDetail.vue";
|
|||||||
import ServerPerformanceDetail from "@/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue";
|
import ServerPerformanceDetail from "@/pages/dashboard/ServerDashboard/ServerPerformanceDetail.vue";
|
||||||
import ServerPartitionsDetail from "@/pages/dashboard/ServerDashboard/ServerPartitionsDetail.vue";
|
import ServerPartitionsDetail from "@/pages/dashboard/ServerDashboard/ServerPartitionsDetail.vue";
|
||||||
import DockerContainerDetail from "@/pages/dashboard/DockerDashboard/DockerContainerDetail.vue";
|
import DockerContainerDetail from "@/pages/dashboard/DockerDashboard/DockerContainerDetail.vue";
|
||||||
|
import {
|
||||||
|
hasProgrammaticBackNavigation,
|
||||||
|
clearProgrammaticBackNavigation,
|
||||||
|
markProgrammaticBackNavigation
|
||||||
|
} from "@/utils/backNavigationSignal";
|
||||||
|
import { useTabBarStore } from "@/store/tabBarStore";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory("/"),
|
history: createWebHistory("/"),
|
||||||
@@ -180,8 +186,60 @@ const router = createRouter({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to: RouteLocationNormalized) => {
|
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<string, number>();
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
markProgrammaticBackNavigation();
|
||||||
|
rawBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 覆盖 go(-1 / -n) 场景,保证与 back() 行为一致。
|
||||||
|
router.go = (delta) => {
|
||||||
|
if (delta < 0) {
|
||||||
|
markProgrammaticBackNavigation();
|
||||||
|
}
|
||||||
|
rawGo(delta);
|
||||||
|
};
|
||||||
|
|
||||||
|
router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized) => {
|
||||||
|
const tabBarStore = useTabBarStore();
|
||||||
const settingStore = useSettingStore();
|
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) {
|
if (to.meta.ignoreConnectCheck) {
|
||||||
return true;
|
return true;
|
||||||
@@ -200,6 +258,15 @@ router.beforeEach((to: RouteLocationNormalized) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach((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();
|
const globalUIStore = useGlobalUIStore();
|
||||||
const targetBackground = typeof to.meta.bodyBackground === "string" ? to.meta.bodyBackground : DEFAULT_BODY_BACKGROUND;
|
const targetBackground = typeof to.meta.bodyBackground === "string" ? to.meta.bodyBackground : DEFAULT_BODY_BACKGROUND;
|
||||||
globalUIStore.setBodyBackground(targetBackground);
|
globalUIStore.setBodyBackground(targetBackground);
|
||||||
|
|||||||
@@ -2,10 +2,23 @@ import { defineStore } from "pinia";
|
|||||||
|
|
||||||
export const useTabBarStore = defineStore("tab-bar", () => {
|
export const useTabBarStore = defineStore("tab-bar", () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// 仅用于“当前这一次”禁用 tabbar 过渡,导航结束后会恢复
|
||||||
|
const shouldSkipTransition = ref(false);
|
||||||
|
|
||||||
const isShowing = computed(() => !!router.currentRoute.value.meta.tabBarVisible);
|
const isShowing = computed(() => !!router.currentRoute.value.meta.tabBarVisible);
|
||||||
|
|
||||||
|
function skipTransitionOnce(): void {
|
||||||
|
shouldSkipTransition.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreTransition(): void {
|
||||||
|
shouldSkipTransition.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isShowing
|
isShowing,
|
||||||
|
shouldSkipTransition,
|
||||||
|
skipTransitionOnce,
|
||||||
|
restoreTransition
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
28
src/utils/backNavigationSignal.ts
Normal file
28
src/utils/backNavigationSignal.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
let programmaticBackAt = 0;
|
||||||
|
|
||||||
|
// 主动返回信号有效期,避免历史信号污染后续导航
|
||||||
|
const PROGRAMMATIC_BACK_SIGNAL_TTL_MS = 1200;
|
||||||
|
|
||||||
|
// 由代码触发 router.back / router.go(-1) 时打标
|
||||||
|
export function markProgrammaticBackNavigation(): void {
|
||||||
|
programmaticBackAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取主动返回信号,超时后自动清理
|
||||||
|
export function hasProgrammaticBackNavigation(): boolean {
|
||||||
|
if (programmaticBackAt < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSignal = Date.now() - programmaticBackAt <= PROGRAMMATIC_BACK_SIGNAL_TTL_MS;
|
||||||
|
if (!hasSignal) {
|
||||||
|
programmaticBackAt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在导航结束后统一清理信号
|
||||||
|
export function clearProgrammaticBackNavigation(): void {
|
||||||
|
programmaticBackAt = 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user