189 行
4.2 KiB
Vue
189 行
4.2 KiB
Vue
<template>
|
||
<Transition name="slide">
|
||
<div v-if="isOpen" id="mobile-nav" class="mobile-nav" role="dialog" aria-label="导航菜单">
|
||
<div class="mobile-nav-backdrop" @click="$emit('close')"></div>
|
||
<nav class="mobile-nav-panel">
|
||
<ul class="mobile-nav-list">
|
||
<li v-for="item in items" :key="item.to" class="mobile-nav-item">
|
||
<!-- 有子菜单的导航项 -->
|
||
<template v-if="item.children">
|
||
<button
|
||
class="mobile-nav-link mobile-nav-toggle"
|
||
:class="{ expanded: expandedItem === item.to }"
|
||
@click="toggleExpand(item.to)"
|
||
>
|
||
{{ item.text }}
|
||
<span class="mobile-nav-arrow">›</span>
|
||
</button>
|
||
<ul v-show="expandedItem === item.to" class="mobile-sub-list">
|
||
<li v-for="child in item.children" :key="child.to" class="mobile-sub-item">
|
||
<NuxtLink :to="child.to" class="mobile-sub-link" @click="$emit('close')">
|
||
<span>{{ child.text }}</span>
|
||
<span v-if="child.season" class="mobile-sub-season">{{ child.season }}</span>
|
||
</NuxtLink>
|
||
</li>
|
||
</ul>
|
||
</template>
|
||
<!-- 普通导航项 -->
|
||
<NuxtLink v-else :to="item.to" class="mobile-nav-link" @click="$emit('close')">
|
||
{{ item.text }}
|
||
</NuxtLink>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
</Transition>
|
||
</template>
|
||
|
||
<script setup>
|
||
const props = defineProps({
|
||
isOpen: { type: Boolean, default: false },
|
||
items: { type: Array, default: () => [] },
|
||
})
|
||
|
||
defineEmits(['close'])
|
||
|
||
const expandedItem = ref(null)
|
||
|
||
function toggleExpand(key) {
|
||
expandedItem.value = expandedItem.value === key ? null : key
|
||
}
|
||
|
||
// 关闭菜单时重置展开状态
|
||
watch(() => props.isOpen, (open) => {
|
||
if (import.meta.client) {
|
||
document.body.style.overflow = open ? 'hidden' : ''
|
||
}
|
||
if (!open) {
|
||
expandedItem.value = null
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (import.meta.client) {
|
||
document.body.style.overflow = ''
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.mobile-nav {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 99;
|
||
}
|
||
|
||
.mobile-nav-backdrop {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.mobile-nav-panel {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: min(300px, 85vw);
|
||
height: 100%;
|
||
background: @color-bg-white;
|
||
padding: @space-3xl @space-lg @space-lg;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.mobile-nav-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-xs;
|
||
}
|
||
|
||
.mobile-nav-link {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
padding: @space-md @space-md;
|
||
font-size: @font-size-md;
|
||
color: @color-text-primary;
|
||
text-decoration: none;
|
||
border-radius: @border-radius-md;
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: background @transition-fast;
|
||
|
||
&:hover {
|
||
background: @color-primary-bg;
|
||
color: @color-primary;
|
||
}
|
||
}
|
||
|
||
.mobile-nav-toggle {
|
||
font-family: inherit;
|
||
}
|
||
|
||
.mobile-nav-arrow {
|
||
font-size: @font-size-lg;
|
||
color: @color-text-muted;
|
||
transition: transform @transition-fast;
|
||
|
||
.expanded & {
|
||
transform: rotate(90deg);
|
||
}
|
||
}
|
||
|
||
.mobile-sub-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-left: @space-md;
|
||
margin-bottom: @space-xs;
|
||
}
|
||
|
||
.mobile-sub-item {
|
||
list-style: none;
|
||
}
|
||
|
||
.mobile-sub-link {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: @space-sm @space-md;
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
text-decoration: none;
|
||
border-radius: @border-radius-sm;
|
||
transition: background @transition-fast, color @transition-fast;
|
||
|
||
&:hover {
|
||
background: @color-primary-bg;
|
||
color: @color-primary;
|
||
}
|
||
}
|
||
|
||
.mobile-sub-season {
|
||
font-size: @font-size-xs;
|
||
color: @color-text-muted;
|
||
background: @color-bg-gray;
|
||
padding: 2px 6px;
|
||
border-radius: @border-radius-sm;
|
||
}
|
||
|
||
.slide-enter-active,
|
||
.slide-leave-active {
|
||
transition: opacity @transition-base;
|
||
|
||
.mobile-nav-panel {
|
||
transition: transform @transition-base;
|
||
}
|
||
}
|
||
|
||
.slide-enter-from,
|
||
.slide-leave-to {
|
||
opacity: 0;
|
||
|
||
.mobile-nav-panel {
|
||
transform: translateX(100%);
|
||
}
|
||
}
|
||
</style>
|