rollback menu layout

This commit is contained in:
jxxghp
2025-03-30 18:02:25 +08:00
parent b8e546a584
commit a909cdc21c
4 changed files with 212 additions and 162 deletions
+186 -69
View File
@@ -1,92 +1,209 @@
<!-- Thanks: https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/ -->
<script lang="ts"> <script lang="ts">
import { Transition } from 'vue' import { Transition } from 'vue'
import { useDisplay } from 'vuetify'
import VerticalNav from '@layouts/components/VerticalNav.vue'
export default defineComponent({ export default defineComponent({
name: 'TransitionExpand', setup(props, { slots }) {
setup(_, { slots }) { const isOverlayNavActive = ref(false)
const onEnter = (element: HTMLElement) => { const isLayoutOverlayVisible = ref(false)
const width = getComputedStyle(element).width const toggleIsOverlayNavActive = useToggle(isOverlayNavActive)
element.style.width = width const route = useRoute()
element.style.position = 'absolute' const { mdAndDown } = useDisplay()
element.style.visibility = 'hidden'
element.style.height = 'auto'
const height = getComputedStyle(element).height // ️ This is alternative to below two commented watcher
// We want to show overlay if overlay nav is visible and want to hide overlay if overlay is hidden and vice versa.
syncRef(isOverlayNavActive, isLayoutOverlayVisible)
element.style.width = '' const scrollDistance = ref(window.scrollY)
element.style.position = ''
element.style.visibility = ''
element.style.height = '0px'
// Force repaint to make sure the onMounted(() => {
// animation is triggered correctly. window.addEventListener('scroll', () => {
// eslint-disable-next-line no-unused-expressions scrollDistance.value = window.scrollY
getComputedStyle(element).height
// Trigger the animation.
// We use `requestAnimationFrame` because we need
// to make sure the browser has finished
// painting after setting the `height`
// to `0` in the line above.
requestAnimationFrame(() => {
element.style.height = height
}) })
}
const onAfterEnter = (element: HTMLElement) => {
element.style.height = 'auto'
}
const onLeave = (element: HTMLElement) => {
const height = getComputedStyle(element).height
element.style.height = height
// Force repaint to make sure the
// animation is triggered correctly.
// eslint-disable-next-line no-unused-expressions
getComputedStyle(element).height
requestAnimationFrame(() => {
element.style.height = '0px'
}) })
}
return () => h( return () => {
h(Transition), // 👉 Vertical nav
const verticalNav = h(
VerticalNav,
{ isOverlayNavActive: isOverlayNavActive.value, toggleIsOverlayNavActive },
{ {
name: 'expand', 'nav-header': () => slots['vertical-nav-header']?.(),
onEnter, 'before-nav-items': () => slots['before-vertical-nav-items']?.(),
onAfterEnter, 'default': () => slots['vertical-nav-content']?.(),
onLeave, 'after-nav-items': () => slots['after-vertical-nav-items']?.(),
}, },
() => slots.default?.(),
) )
// 👉 Navbar
const navbar = h('header', { class: ['layout-navbar navbar-blur'] }, [
h(
'div',
{ class: 'navbar-content-container' },
slots.navbar?.({
toggleVerticalOverlayNavActive: toggleIsOverlayNavActive,
}),
),
])
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?.()),
),
)
// 👉 根据路由 meta 决定 footer 高度
const shouldShowFooter = !route.meta.hideFooter
// 👉 Footer
const footer = h('footer', { class: 'layout-footer' }, [
h(
'div',
{
class: ['footer-content-container', !shouldShowFooter && 'footer-content-container-noheight'],
},
slots.footer?.(),
),
])
// 👉 Overlay
const layoutOverlay = h('div', {
class: ['layout-overlay', 'touch-none', { visible: isLayoutOverlayVisible.value }],
onClick: () => {
isLayoutOverlayVisible.value = !isLayoutOverlayVisible.value
},
})
return h(
'div',
{
class: [
'layout-wrapper layout-nav-type-vertical layout-navbar-static layout-footer-static layout-content-width-fluid',
'layout-navbar-fixed',
mdAndDown.value && 'layout-overlay-nav',
route.meta.layoutWrapperClasses,
scrollDistance.value && 'window-scrolled',
],
},
[verticalNav, h('div', { class: 'layout-content-wrapper' }, [navbar, main, footer]), layoutOverlay],
)
}
}, },
}) })
</script> </script>
<style> <style lang="scss">
.expand-enter-active, @use '@configured-variables' as variables;
.expand-leave-active { @use '@layouts/styles/placeholders';
@use '@layouts/styles/mixins';
.layout-wrapper.layout-nav-type-vertical {
// TODO(v2): Check why we need height in vertical nav & min-height in horizontal nav
block-size: 100%;
.layout-content-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
min-block-size: calc(var(--vh, 1vh) * 100);
transition: padding-inline-start 0.2s ease-in-out;
will-change: padding-inline-start;
}
.layout-navbar {
position: fixed;
width: calc(100vw - variables.$layout-vertical-nav-width - 0.5rem);
z-index: variables.$layout-vertical-nav-layout-navbar-z-index;
inset-block-start: 0;
.navbar-content-container {
block-size: calc(env(safe-area-inset-top) + variables.$layout-vertical-nav-navbar-height);
}
@at-root {
.layout-wrapper.layout-nav-type-vertical {
.layout-navbar {
@if variables.$layout-vertical-nav-navbar-is-contained {
@include mixins.boxed-content;
} @else {
.navbar-content-container {
@include mixins.boxed-content;
}
}
}
}
}
}
&.layout-navbar-fixed .layout-navbar {
@extend %layout-navbar-fixed;
}
&.layout-navbar-hidden .layout-navbar {
@extend %layout-navbar-hidden;
}
// 👉 Footer
.layout-footer {
@include mixins.boxed-content;
}
// 👉 Layout overlay
.layout-overlay {
position: fixed;
z-index: variables.$layout-overlay-z-index;
background-color: rgb(0 0 0 / 60%);
cursor: pointer;
inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease-in-out;
will-change: transform;
&.visible {
opacity: 1;
pointer-events: auto;
}
}
&:not(.layout-overlay-nav) .layout-content-wrapper {
padding-inline-start: variables.$layout-vertical-nav-width;
}
// Adjust right column pl when vertical nav is collapsed
&.layout-vertical-nav-collapsed .layout-content-wrapper {
padding-inline-start: variables.$layout-vertical-nav-collapsed-width;
}
// 👉 Content height fixed
&.layout-content-height-fixed {
.layout-content-wrapper {
max-block-size: calc(var(--vh) * 100);
}
.layout-page-content {
// display: flex;
overflow: hidden; overflow: hidden;
transition: block-size var(--expand-transition-duration, 0.25s) ease;
.page-content-container {
inline-size: 100%;
> :first-child {
max-block-size: 100%;
overflow-y: auto;
}
}
}
}
} }
.expand-enter-from, .layout-wrapper.layout-nav-type-vertical.layout-overlay-nav {
.expand-leave-to { .layout-navbar {
block-size: 0; width: 100%;
} }
</style>
<style scoped>
* {
backface-visibility: hidden;
perspective: 62.5rem;
transform: translateZ(0);
will-change: block-size;
} }
</style> </style>
+4 -14
View File
@@ -117,23 +117,13 @@ function handleNavScroll(evt: Event) {
} }
.nav-items { .nav-items {
overflow-x: hidden;
overflow-y: auto;
block-size: 100%; block-size: 100%;
display: flex;
flex-direction: column;
padding: 0 0 16px 0;
/* 完全隐藏滚动条 */ // ️ We no loner needs this overflow styles as perfect scrollbar applies it
&::-webkit-scrollbar { // overflow-x: hidden;
display: none;
}
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
> li { // // ️ We used `overflow-y` instead of `overflow` to mitigate overflow x. Revert back if any issue found.
margin-block-end: 2px; // overflow-y: auto;
}
} }
.nav-item-title { .nav-item-title {
+9 -63
View File
@@ -1,29 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { NavLink } from '@layouts/types' import type { NavLink } from '@layouts/types'
// 定义类型必须使用vuetify中正确导出的类型 defineProps<{
const props = defineProps<{
item: NavLink item: NavLink
}>() }>()
</script> </script>
<template> <template>
<li <li class="nav-link" :class="{ disabled: item.disable }">
class="nav-link" <Component :is="item.to ? 'RouterLink' : 'a'" :to="item.to" :href="item.href">
:class="{ disabled: item.disable }" <VIcon :icon="item.icon" class="nav-item-icon" />
> <!-- 👉 Title -->
<Component
:is="item.to ? 'RouterLink' : 'a'"
:to="item.to"
:href="item.href"
class="link-wrapper"
>
<VIcon
v-if="item.icon != null"
:icon="item.icon?.toString()"
size="20"
class="nav-item-icon"
/>
<span class="nav-item-title"> <span class="nav-item-title">
{{ item.title }} {{ item.title }}
</span> </span>
@@ -33,54 +20,13 @@ const props = defineProps<{
<style lang="scss"> <style lang="scss">
.layout-vertical-nav { .layout-vertical-nav {
.nav-link { .nav-link a {
margin: 1px 16px;
position: relative;
&.disabled {
opacity: 0.6;
pointer-events: none;
}
.router-link-active {
background-color: rgb(var(--v-theme-primary));
position: relative;
border-radius: 6px;
.nav-item-icon,
.nav-item-title {
color: white;
}
}
a, .link-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
border-radius: 6px; border-radius: 0 3.125rem 3.125rem 0 !important;
cursor: pointer; cursor: pointer;
padding: 8px 10px; margin-inline-end: 1.125em;
transition: background-color 0.2s ease; padding-inline: 1.375rem 1rem;
position: relative;
&:hover:not(.router-link-active) {
background-color: rgba(var(--v-theme-on-surface), 0.05);
}
}
}
.nav-item-icon {
color: rgba(var(--v-theme-on-surface), 0.75);
margin-right: 8px;
min-width: 20px;
}
.nav-item-title {
font-size: 0.85rem;
font-weight: 500;
color: rgba(var(--v-theme-on-surface), 0.85);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
} }
</style> </style>
@@ -8,22 +8,19 @@ defineProps<{
<template> <template>
<li class="nav-section-title"> <li class="nav-section-title">
<div class="title-text">{{ item.heading }}</div> <div class="title-wrapper">
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<span class="title-text" v-text="item.heading" />
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
</div>
</li> </li>
</template> </template>
<style lang="scss"> <style lang="scss">
.layout-vertical-nav { .layout-vertical-nav {
.nav-section-title { .nav-section-title {
margin: 16px 16px 6px 16px; padding-left: 1.375rem;
position: relative; padding-right: 1rem;
.title-text {
font-size: 0.75rem;
font-weight: 600;
color: rgb(var(--v-theme-primary));
padding-left: 0;
}
} }
} }
</style> </style>