180 lines
4.1 KiB
Vue
180 lines
4.1 KiB
Vue
<template>
|
|
<div class="page-transition">
|
|
<router-view v-slot="{ Component }">
|
|
<transition :name="transitionName">
|
|
<div :key="route.fullPath" class="pages">
|
|
<component :is="Component" />
|
|
</div>
|
|
</transition>
|
|
</router-view>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { RouteLocationNormalized } from "vue-router";
|
|
import { useGlobalUIStore } from "@/store/globalUI";
|
|
|
|
defineOptions({
|
|
name: "PageTransition"
|
|
});
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const globalUIStore = useGlobalUIStore();
|
|
|
|
const transitionName = ref("push-left");
|
|
const pageBackground = computed(() => globalUIStore.bodyBackground);
|
|
|
|
// ---------- 路由深度计算 ----------
|
|
|
|
const pathCache = new Map<string, number>();
|
|
|
|
function calcDepth(sourceRoute: RouteLocationNormalized): number {
|
|
const queryDepth = parseTransitionDepth(sourceRoute.query.transitionDepth);
|
|
if (queryDepth !== undefined) {
|
|
return queryDepth;
|
|
}
|
|
|
|
if (!sourceRoute.meta.dynamicDepth) {
|
|
return Number(sourceRoute.meta.depth ?? 0);
|
|
}
|
|
|
|
const fullPath = sourceRoute.fullPath;
|
|
const cachedDepth = pathCache.get(fullPath);
|
|
if (cachedDepth !== undefined) {
|
|
return cachedDepth;
|
|
}
|
|
|
|
const baseDepth = Number(sourceRoute.meta.baseDepth ?? 0);
|
|
const pathSegments = fullPath.split("?")[0].split("/").filter((pathSegment) => pathSegment !== "");
|
|
const depth = baseDepth + (pathSegments.length - 1);
|
|
pathCache.set(fullPath, depth);
|
|
return depth;
|
|
}
|
|
|
|
function parseTransitionDepth(value: unknown): number | undefined {
|
|
if (Array.isArray(value)) {
|
|
return parseTransitionDepth(value[0]);
|
|
}
|
|
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
|
|
const text = value.trim();
|
|
if (!text) {
|
|
return undefined;
|
|
}
|
|
|
|
const depth = Number(text);
|
|
if (!Number.isInteger(depth) || depth < 0 || 20 < depth) {
|
|
return undefined;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
const unregisterGuard = router.beforeEach((to, from) => {
|
|
const toDepth = calcDepth(to);
|
|
const fromDepth = calcDepth(from);
|
|
|
|
if (fromDepth < toDepth) {
|
|
transitionName.value = "push-left";
|
|
} else if (toDepth < fromDepth) {
|
|
transitionName.value = "push-right";
|
|
} else {
|
|
transitionName.value = "";
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
unregisterGuard();
|
|
pathCache.clear();
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="less">
|
|
@easing: cubic-bezier(.19, 1, .22, 1);
|
|
@duration: 520ms;
|
|
@brightness: .98;
|
|
@shadow-intensity: .15;
|
|
@page-offset: 25%;
|
|
|
|
.page-transition {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
overflow-x: hidden;
|
|
|
|
// 此 wrapper 必须独立,不可将 class=pages 直接加在 <component> 上:
|
|
// 否则页面 root element 与动画容器同为一个 DOM 节点,
|
|
// 页面自身的 padding 会使 position:fixed; width:100% 的盒子溢出视口。
|
|
.pages {
|
|
height: 100%;
|
|
background: v-bind(pageBackground);
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
// ---------- push-left / push-right 公共激活状态 ----------
|
|
|
|
.push-left-enter-active,
|
|
.push-left-leave-active,
|
|
.push-right-enter-active,
|
|
.push-right-leave-active {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: fixed;
|
|
transition: transform @duration @easing, opacity .2s linear;
|
|
backface-visibility: hidden;
|
|
}
|
|
|
|
// ---------- 前进:下一页从右滑入,当前页缩退到左侧 ----------
|
|
|
|
.push-left-enter-from {
|
|
transform: translate3d(100%, 0, 0);
|
|
box-shadow: -4px 0 16px rgba(0, 0, 0, @shadow-intensity);
|
|
}
|
|
|
|
.push-left-enter-to {
|
|
transform: translate3d(0, 0, 0);
|
|
}
|
|
|
|
.push-left-leave-active {
|
|
transition: all @duration @easing, filter .3s;
|
|
}
|
|
|
|
.push-left-leave-to {
|
|
filter: brightness(@brightness);
|
|
transform: translate3d(-@page-offset, 0, 0);
|
|
}
|
|
|
|
// ---------- 返回:当前页从右滑出,上一页从左侧复位 ----------
|
|
|
|
.push-right-enter-active {
|
|
z-index: 0;
|
|
transition: transform @duration @easing, opacity .3s;
|
|
}
|
|
|
|
.push-right-enter-from {
|
|
transform: translate3d(-@page-offset, 0, 0);
|
|
}
|
|
|
|
.push-right-enter-to {
|
|
transform: translate3d(0, 0, 0);
|
|
}
|
|
|
|
.push-right-leave-active {
|
|
z-index: 1;
|
|
transition: transform @duration @easing, box-shadow .3s;
|
|
}
|
|
|
|
.push-right-leave-from {
|
|
transform: translate3d(0, 0, 0);
|
|
}
|
|
|
|
.push-right-leave-to {
|
|
transform: translate3d(105%, 0, 0);
|
|
box-shadow: 4px 0 16px rgba(0, 0, 0, @shadow-intensity);
|
|
}
|
|
}
|
|
</style>
|