init project

This commit is contained in:
Timi
2026-04-03 12:02:34 +08:00
parent d4bef26c96
commit 2665acc885
36 changed files with 5725 additions and 218 deletions

View File

@@ -0,0 +1,156 @@
<template>
<div class="page-transition">
<router-view v-slot="{ Component }">
<transition v-if="hasTransition" :name="transitionName">
<div class="pages" :key="route.fullPath">
<component :is="Component" />
</div>
</transition>
<div v-else class="pages" :key="route.fullPath">
<component :is="Component" />
</div>
</router-view>
</div>
</template>
<script lang="ts" setup>
import type { RouteLocationNormalized } from "vue-router";
import { useGlobalUIStore } from "@/store/globalUIStore";
defineOptions({
name: "PageTransition"
});
const router = useRouter();
const route = useRoute();
const globalUIStore = useGlobalUIStore();
const transitionName = ref("push-left");
const hasTransition = computed(() => transitionName.value !== "");
const pageBackground = computed(() => globalUIStore.bodyBackground);
// ---------- 路由深度计算 ----------
const pathCache = new Map<string, number>();
function calcDepth(sourceRoute: RouteLocationNormalized): number {
if (!sourceRoute.meta.dynamicDepth) {
return Number(sourceRoute.meta.depth ?? 0);
}
const fullPath = sourceRoute.fullPath;
const cached = pathCache.get(fullPath);
if (cached !== undefined) {
return cached;
}
const base = Number(sourceRoute.meta.baseDepth ?? 0);
const pathSegments = fullPath.split("?")[0].split("/").filter((pathSegment) => pathSegment !== "");
const depth = base + (pathSegments.length - 1);
pathCache.set(fullPath, depth);
return depth;
}
// beforeEach 返回注销函数,组件卸载时必须调用,否则守卫会一直存在。
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 lang="less" scoped>
@easing: cubic-bezier(.19, 1, .22, 1);
@duration: 750ms;
@brightness: .98;
@shadow-intensity: .15;
@page-offset: 25%;
.page-transition {
width: 100%;
height: 100%;
position: relative;
overflow-x: hidden;
// 这个 wrapper 必须独立,不能把 class=pages 直接挂在 <component> 上。
// 否则页面根节点会和动画容器变成同一个 DOM 节点,容易导致 fixed 布局溢出视口。
.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: -1;
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 {
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>

View File

@@ -0,0 +1,63 @@
<template>
<section class="placeholder">
<div class="box">
<p class="tag">当前组件</p>
<h1 class="title" v-text="title"></h1>
<p class="desc" v-text="description"></p>
</div>
</section>
</template>
<script setup lang="ts">
defineProps<{
title: string;
description: string;
}>();
</script>
<style scoped lang="less">
.placeholder {
width: 100%;
min-height: 100%;
padding: 1.2rem;
.box {
gap: .9rem;
display: flex;
min-height: 12rem;
padding: 1.2rem;
border-radius: 1rem;
flex-direction: column;
justify-content: center;
border: 1px solid var(--app-line);
background: linear-gradient(160deg, rgba(255, 255, 255, .92), rgba(255, 255, 255, .72));
box-shadow: 0 .4rem 1.2rem rgba(17, 32, 56, .08);
}
.tag {
margin: 0;
color: var(--app-primary);
font-size: .875rem;
}
.title {
margin: 0;
font-size: 1.6rem;
line-height: 1.2;
}
.desc {
margin: 0;
color: var(--app-sub);
font-size: .95rem;
line-height: 1.6;
}
}
:global(.theme-dark) .placeholder {
.box {
background: linear-gradient(160deg, rgba(29, 37, 47, .96), rgba(22, 28, 36, .88));
box-shadow: 0 .4rem 1.2rem rgba(0, 0, 0, .28);
}
}
</style>