mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-16 21:20:45 +08:00
feat: enhance page transition animations and overlay effects
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { Transition } from 'vue'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
||||
import {
|
||||
@@ -110,9 +109,7 @@ export default defineComponent({
|
||||
const main = h(
|
||||
'main',
|
||||
{ class: 'layout-page-content' },
|
||||
h(Transition, { name: 'fade-slide', mode: 'out-in', appear: true }, () =>
|
||||
h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
),
|
||||
h('section', { class: 'page-content-container' }, slots.default?.()),
|
||||
)
|
||||
|
||||
// 👉 根据路由 meta 决定 footer 高度
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
const route = useRoute()
|
||||
|
||||
// 空白布局用于登录、初始化与 404,复用全局页面动效保持切换手感一致。
|
||||
const routeTransitionKey = computed(() => route.fullPath)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-wrapper layout-blank">
|
||||
<RouterView />
|
||||
<RouterView v-slot="{ Component }">
|
||||
<transition name="mp-page" mode="out-in" appear>
|
||||
<div :key="routeTransitionKey" class="mp-page-route">
|
||||
<component :is="Component" />
|
||||
</div>
|
||||
</transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,15 +5,55 @@ const route = useRoute()
|
||||
|
||||
// keep-alive 缓存按页面身份命中,避免 query 变化导致同一页面反复新建实例。
|
||||
const routeCacheKey = computed(() => route.meta.keepAliveKey?.toString() || route.path)
|
||||
|
||||
// 页面过渡按实际页面身份触发;keep-alive 页面避免 query 变化时反复入场。
|
||||
const routeTransitionKey = computed(() => (route.meta.keepAlive ? routeCacheKey.value : route.fullPath))
|
||||
const isPageEntering = ref(false)
|
||||
let pageMotionTimer: number | null = null
|
||||
let pageMotionFrame: number | null = null
|
||||
|
||||
// 使用稳定容器触发轻量入场动画,避免重建 keep-alive 导致页面缓存失效。
|
||||
function playPageEnterMotion() {
|
||||
if (pageMotionTimer) {
|
||||
window.clearTimeout(pageMotionTimer)
|
||||
pageMotionTimer = null
|
||||
}
|
||||
|
||||
if (pageMotionFrame) {
|
||||
window.cancelAnimationFrame(pageMotionFrame)
|
||||
pageMotionFrame = null
|
||||
}
|
||||
|
||||
isPageEntering.value = false
|
||||
pageMotionFrame = window.requestAnimationFrame(() => {
|
||||
isPageEntering.value = true
|
||||
pageMotionFrame = null
|
||||
pageMotionTimer = window.setTimeout(() => {
|
||||
isPageEntering.value = false
|
||||
pageMotionTimer = null
|
||||
}, 220)
|
||||
})
|
||||
}
|
||||
|
||||
watch(routeTransitionKey, playPageEnterMotion, { flush: 'post' })
|
||||
|
||||
onMounted(playPageEnterMotion)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (pageMotionTimer) window.clearTimeout(pageMotionTimer)
|
||||
if (pageMotionFrame) window.cancelAnimationFrame(pageMotionFrame)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :max="24">
|
||||
<component :is="Component" v-if="route.meta.keepAlive" :key="routeCacheKey" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
|
||||
<div class="mp-page-route" :class="{ 'mp-page-route--entering': isPageEntering }">
|
||||
<keep-alive :max="24">
|
||||
<component :is="Component" v-if="route.meta.keepAlive" :key="routeCacheKey" />
|
||||
</keep-alive>
|
||||
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
|
||||
</div>
|
||||
</router-view>
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
|
||||
@@ -34,16 +34,19 @@ export default {
|
||||
},
|
||||
VMenu: {
|
||||
elevation: 0,
|
||||
transition: 'mp-popover-transition',
|
||||
},
|
||||
VChip: {
|
||||
elevation: 0,
|
||||
},
|
||||
VBottomSheet: {
|
||||
elevation: 0,
|
||||
transition: 'mp-bottom-sheet-transition',
|
||||
},
|
||||
VDialog: {
|
||||
elevation: 0,
|
||||
rounded: 'lg',
|
||||
transition: 'mp-dialog-transition',
|
||||
},
|
||||
VExpansionPanels: {
|
||||
elevation: 0,
|
||||
@@ -68,6 +71,7 @@ export default {
|
||||
VTooltip: {
|
||||
// set v-tooltip default location to top
|
||||
location: 'top',
|
||||
transition: 'mp-popover-transition',
|
||||
},
|
||||
VCheckboxBtn: {
|
||||
color: 'primary',
|
||||
|
||||
@@ -50,6 +50,10 @@ html {
|
||||
--app-overlay-shadow: none;
|
||||
--app-surface-shadow: none;
|
||||
--app-surface-hover-shadow: none;
|
||||
--mp-motion-duration-page: 180ms;
|
||||
--mp-motion-duration-overlay: 160ms;
|
||||
--mp-motion-ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
--mp-motion-ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
|
||||
html[data-theme-skin='bordered'] {
|
||||
@@ -156,6 +160,190 @@ html[data-theme-shadow='high'] {
|
||||
box-shadow: var(--app-surface-shadow) !important;
|
||||
}
|
||||
|
||||
// 全局页面与 overlay 动效:短距离、轻缩放,保持快速但不生硬。
|
||||
.mp-page-route {
|
||||
inline-size: 100%;
|
||||
min-block-size: 100%;
|
||||
}
|
||||
|
||||
.mp-page-route--entering {
|
||||
animation: mp-page-route-enter var(--mp-motion-duration-page) var(--mp-motion-ease-standard) both;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
@keyframes mp-page-route-enter {
|
||||
from {
|
||||
filter: blur(1px);
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0.5rem, 0) scale(0.992);
|
||||
}
|
||||
|
||||
to {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.mp-page-enter-active,
|
||||
.mp-page-leave-active {
|
||||
backface-visibility: hidden;
|
||||
transform-origin: center top;
|
||||
transition:
|
||||
opacity var(--mp-motion-duration-page) var(--mp-motion-ease-standard),
|
||||
transform var(--mp-motion-duration-page) var(--mp-motion-ease-standard),
|
||||
filter var(--mp-motion-duration-page) var(--mp-motion-ease-standard);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.mp-page-leave-active {
|
||||
transition-duration: 120ms;
|
||||
transition-timing-function: var(--mp-motion-ease-exit);
|
||||
}
|
||||
|
||||
.mp-page-enter-from {
|
||||
filter: blur(1px);
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0.5rem, 0) scale(0.992);
|
||||
}
|
||||
|
||||
.mp-page-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -0.25rem, 0) scale(0.998);
|
||||
}
|
||||
|
||||
.mp-dialog-transition-enter-active,
|
||||
.mp-dialog-transition-leave-active,
|
||||
.dialog-transition-enter-active,
|
||||
.dialog-transition-leave-active,
|
||||
.v-overlay__content.mp-dialog-transition-enter-active,
|
||||
.v-overlay__content.mp-dialog-transition-leave-active,
|
||||
.v-overlay__content.dialog-transition-enter-active,
|
||||
.v-overlay__content.dialog-transition-leave-active {
|
||||
transition:
|
||||
opacity var(--mp-motion-duration-overlay) var(--mp-motion-ease-standard),
|
||||
transform var(--mp-motion-duration-overlay) var(--mp-motion-ease-standard),
|
||||
filter var(--mp-motion-duration-overlay) var(--mp-motion-ease-standard);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.mp-dialog-transition-leave-active,
|
||||
.dialog-transition-leave-active {
|
||||
transition-duration: 120ms;
|
||||
transition-timing-function: var(--mp-motion-ease-exit);
|
||||
}
|
||||
|
||||
.mp-dialog-transition-enter-from,
|
||||
.mp-dialog-transition-leave-to,
|
||||
.dialog-transition-enter-from,
|
||||
.dialog-transition-leave-to {
|
||||
filter: blur(0.5px);
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0.625rem, 0) scale(0.985);
|
||||
}
|
||||
|
||||
.mp-popover-transition-enter-active,
|
||||
.mp-popover-transition-leave-active,
|
||||
.v-overlay__content.mp-popover-transition-enter-active,
|
||||
.v-overlay__content.mp-popover-transition-leave-active {
|
||||
transition:
|
||||
opacity 120ms var(--mp-motion-ease-standard),
|
||||
transform 120ms var(--mp-motion-ease-standard);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.mp-popover-transition-leave-active {
|
||||
transition-duration: 90ms;
|
||||
transition-timing-function: var(--mp-motion-ease-exit);
|
||||
}
|
||||
|
||||
.mp-popover-transition-enter-from,
|
||||
.mp-popover-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -0.25rem, 0) scale(0.98);
|
||||
}
|
||||
|
||||
.mp-bottom-sheet-transition-enter-active,
|
||||
.mp-bottom-sheet-transition-leave-active,
|
||||
.dialog-bottom-transition-enter-active,
|
||||
.dialog-bottom-transition-leave-active,
|
||||
.v-overlay__content.mp-bottom-sheet-transition-enter-active,
|
||||
.v-overlay__content.mp-bottom-sheet-transition-leave-active,
|
||||
.v-overlay__content.dialog-bottom-transition-enter-active,
|
||||
.v-overlay__content.dialog-bottom-transition-leave-active {
|
||||
transition:
|
||||
opacity var(--mp-motion-duration-overlay) var(--mp-motion-ease-standard),
|
||||
transform var(--mp-motion-duration-overlay) var(--mp-motion-ease-standard);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.mp-bottom-sheet-transition-leave-active,
|
||||
.dialog-bottom-transition-leave-active {
|
||||
transition-duration: 120ms;
|
||||
transition-timing-function: var(--mp-motion-ease-exit);
|
||||
}
|
||||
|
||||
.mp-bottom-sheet-transition-enter-from,
|
||||
.mp-bottom-sheet-transition-leave-to,
|
||||
.dialog-bottom-transition-enter-from,
|
||||
.dialog-bottom-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 1.25rem, 0);
|
||||
}
|
||||
|
||||
.v-dialog > .v-overlay__content {
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.v-menu > .v-overlay__content,
|
||||
.v-tooltip > .v-overlay__content {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.v-overlay__scrim,
|
||||
.v-navigation-drawer__scrim {
|
||||
transition:
|
||||
background-color var(--mp-motion-duration-overlay) var(--mp-motion-ease-standard),
|
||||
opacity var(--mp-motion-duration-overlay) var(--mp-motion-ease-standard);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mp-page-route--entering {
|
||||
animation-duration: 1ms !important;
|
||||
}
|
||||
|
||||
.mp-page-enter-active,
|
||||
.mp-page-leave-active,
|
||||
.mp-dialog-transition-enter-active,
|
||||
.mp-dialog-transition-leave-active,
|
||||
.dialog-transition-enter-active,
|
||||
.dialog-transition-leave-active,
|
||||
.mp-popover-transition-enter-active,
|
||||
.mp-popover-transition-leave-active,
|
||||
.mp-bottom-sheet-transition-enter-active,
|
||||
.mp-bottom-sheet-transition-leave-active,
|
||||
.dialog-bottom-transition-enter-active,
|
||||
.dialog-bottom-transition-leave-active {
|
||||
transition-duration: 1ms !important;
|
||||
}
|
||||
|
||||
.mp-page-enter-from,
|
||||
.mp-page-leave-to,
|
||||
.mp-dialog-transition-enter-from,
|
||||
.mp-dialog-transition-leave-to,
|
||||
.dialog-transition-enter-from,
|
||||
.dialog-transition-leave-to,
|
||||
.mp-popover-transition-enter-from,
|
||||
.mp-popover-transition-leave-to,
|
||||
.mp-bottom-sheet-transition-enter-from,
|
||||
.mp-bottom-sheet-transition-leave-to,
|
||||
.dialog-bottom-transition-enter-from,
|
||||
.dialog-bottom-transition-leave-to {
|
||||
filter: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部导航卡片原本就是胶囊形态,不参与主题圆角切换。
|
||||
.footer-nav-card.v-card {
|
||||
--app-surface-radius: 9999px;
|
||||
|
||||
Reference in New Issue
Block a user