diff --git a/src/@layouts/components/VerticalNavLayout.vue b/src/@layouts/components/VerticalNavLayout.vue
index 2cc24ad6..0d412ac6 100644
--- a/src/@layouts/components/VerticalNavLayout.vue
+++ b/src/@layouts/components/VerticalNavLayout.vue
@@ -1,5 +1,4 @@
+
diff --git a/src/layouts/default.vue b/src/layouts/default.vue
index 702ae04b..e24c9d01 100644
--- a/src/layouts/default.vue
+++ b/src/layouts/default.vue
@@ -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)
+})
-
-
-
-
+
+
+
+
+
+
diff --git a/src/plugins/vuetify/defaults.ts b/src/plugins/vuetify/defaults.ts
index 3ad423a9..e958bb62 100644
--- a/src/plugins/vuetify/defaults.ts
+++ b/src/plugins/vuetify/defaults.ts
@@ -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',
diff --git a/src/styles/common.scss b/src/styles/common.scss
index ea70839d..fa5835b5 100644
--- a/src/styles/common.scss
+++ b/src/styles/common.scss
@@ -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;