mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
feat: enhance page transition animations and overlay effects
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Transition } from 'vue'
|
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
import VerticalNav from '@layouts/components/VerticalNav.vue'
|
||||||
import {
|
import {
|
||||||
@@ -110,9 +109,7 @@ export default defineComponent({
|
|||||||
const main = h(
|
const main = h(
|
||||||
'main',
|
'main',
|
||||||
{ class: 'layout-page-content' },
|
{ 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 高度
|
// 👉 根据路由 meta 决定 footer 高度
|
||||||
|
|||||||
+14
-1
@@ -1,6 +1,19 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 空白布局用于登录、初始化与 404,复用全局页面动效保持切换手感一致。
|
||||||
|
const routeTransitionKey = computed(() => route.fullPath)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="layout-wrapper layout-blank">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+44
-4
@@ -5,15 +5,55 @@ const route = useRoute()
|
|||||||
|
|
||||||
// keep-alive 缓存按页面身份命中,避免 query 变化导致同一页面反复新建实例。
|
// keep-alive 缓存按页面身份命中,避免 query 变化导致同一页面反复新建实例。
|
||||||
const routeCacheKey = computed(() => route.meta.keepAliveKey?.toString() || route.path)
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive :max="24">
|
<div class="mp-page-route" :class="{ 'mp-page-route--entering': isPageEntering }">
|
||||||
<component :is="Component" v-if="route.meta.keepAlive" :key="routeCacheKey" />
|
<keep-alive :max="24">
|
||||||
</keep-alive>
|
<component :is="Component" v-if="route.meta.keepAlive" :key="routeCacheKey" />
|
||||||
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
|
</keep-alive>
|
||||||
|
<component :is="Component" v-if="!route.meta.keepAlive" :key="route.fullPath" />
|
||||||
|
</div>
|
||||||
</router-view>
|
</router-view>
|
||||||
</DefaultLayout>
|
</DefaultLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -34,16 +34,19 @@ export default {
|
|||||||
},
|
},
|
||||||
VMenu: {
|
VMenu: {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
transition: 'mp-popover-transition',
|
||||||
},
|
},
|
||||||
VChip: {
|
VChip: {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
},
|
},
|
||||||
VBottomSheet: {
|
VBottomSheet: {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
transition: 'mp-bottom-sheet-transition',
|
||||||
},
|
},
|
||||||
VDialog: {
|
VDialog: {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
rounded: 'lg',
|
rounded: 'lg',
|
||||||
|
transition: 'mp-dialog-transition',
|
||||||
},
|
},
|
||||||
VExpansionPanels: {
|
VExpansionPanels: {
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -68,6 +71,7 @@ export default {
|
|||||||
VTooltip: {
|
VTooltip: {
|
||||||
// set v-tooltip default location to top
|
// set v-tooltip default location to top
|
||||||
location: 'top',
|
location: 'top',
|
||||||
|
transition: 'mp-popover-transition',
|
||||||
},
|
},
|
||||||
VCheckboxBtn: {
|
VCheckboxBtn: {
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ html {
|
|||||||
--app-overlay-shadow: none;
|
--app-overlay-shadow: none;
|
||||||
--app-surface-shadow: none;
|
--app-surface-shadow: none;
|
||||||
--app-surface-hover-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'] {
|
html[data-theme-skin='bordered'] {
|
||||||
@@ -156,6 +160,190 @@ html[data-theme-shadow='high'] {
|
|||||||
box-shadow: var(--app-surface-shadow) !important;
|
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 {
|
.footer-nav-card.v-card {
|
||||||
--app-surface-radius: 9999px;
|
--app-surface-radius: 9999px;
|
||||||
|
|||||||
Reference in New Issue
Block a user