mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-03 00:02:28 +08:00
refactor(frontend): optimize layout (remove MobileNav, enhance MainLayout)
This commit is contained in:
@@ -1,127 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="lg:hidden">
|
|
||||||
<div class="sticky top-0 z-40 space-y-4 pb-4">
|
|
||||||
<!-- Logo头部 - 移动端优化 -->
|
|
||||||
<div class="flex items-center gap-3 rounded-2xl bg-card/90 px-4 py-3 shadow-lg shadow-primary/20 ring-1 ring-border backdrop-blur">
|
|
||||||
<div class="flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center rounded-2xl bg-background shadow-md shadow-primary/20 flex-shrink-0">
|
|
||||||
<img
|
|
||||||
src="/aether_adaptive.svg"
|
|
||||||
alt="Logo"
|
|
||||||
class="h-8 w-8 sm:h-10 sm:w-10"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<!-- 文字部分 - 小屏隐藏 -->
|
|
||||||
<div class="hidden sm:block">
|
|
||||||
<p class="text-sm font-semibold text-foreground">
|
|
||||||
Aether
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
AI 控制中心
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex w-full items-center gap-3 rounded-2xl bg-card/80 px-4 py-3 shadow-lg shadow-primary/20 ring-1 ring-border backdrop-blur transition hover:ring-primary/30"
|
|
||||||
@click="toggleMenu"
|
|
||||||
>
|
|
||||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary flex-shrink-0">
|
|
||||||
<Menu class="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 flex-col text-left min-w-0">
|
|
||||||
<span class="text-sm font-semibold text-foreground truncate">
|
|
||||||
快速导航
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-muted-foreground truncate">
|
|
||||||
{{ activeItem ? `当前:${activeItem.name}` : '选择功能页面' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ChevronDown
|
|
||||||
class="h-4 w-4 text-muted-foreground transition-transform duration-200 flex-shrink-0"
|
|
||||||
:class="{ 'rotate-180': isOpen }"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
enter-active-class="transition duration-200 ease-out"
|
|
||||||
enter-from-class="opacity-0 -translate-y-2 scale-95"
|
|
||||||
enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
||||||
leave-active-class="transition duration-150 ease-in"
|
|
||||||
leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
||||||
leave-to-class="opacity-0 -translate-y-1 scale-95"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="isOpen"
|
|
||||||
class="space-y-3 rounded-3xl bg-card/95 p-4 shadow-2xl ring-1 ring-border backdrop-blur-xl"
|
|
||||||
>
|
|
||||||
<SidebarNav
|
|
||||||
:items="props.items"
|
|
||||||
:is-active="isLinkActive"
|
|
||||||
:active-path="props.activePath"
|
|
||||||
list-class="space-y-2"
|
|
||||||
@navigate="handleNavigate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Component } from 'vue'
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { ChevronDown, Menu } from 'lucide-vue-next'
|
|
||||||
import SidebarNav from '@/components/layout/SidebarNav.vue'
|
|
||||||
|
|
||||||
export interface NavigationItem {
|
|
||||||
name: string
|
|
||||||
href: string
|
|
||||||
icon: Component
|
|
||||||
description?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NavigationGroup {
|
|
||||||
title?: string
|
|
||||||
items: NavigationItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
items: NavigationGroup[]
|
|
||||||
activePath?: string
|
|
||||||
isActive?: (href: string) => boolean
|
|
||||||
isDark?: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const isOpen = ref(false)
|
|
||||||
|
|
||||||
const activeItem = computed(() => {
|
|
||||||
for (const group of props.items) {
|
|
||||||
const found = group.items.find(item => isLinkActive(item.href))
|
|
||||||
if (found) return found
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
function isLinkActive(href: string) {
|
|
||||||
if (props.isActive) {
|
|
||||||
return props.isActive(href)
|
|
||||||
}
|
|
||||||
if (props.activePath) {
|
|
||||||
return props.activePath === href || props.activePath.startsWith(`${href}/`)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMenu() {
|
|
||||||
isOpen.value = !isOpen.value
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNavigate() {
|
|
||||||
isOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.activePath, () => {
|
|
||||||
isOpen.value = false
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -11,5 +11,4 @@ export { default as CardSection } from './CardSection.vue'
|
|||||||
|
|
||||||
// 应用外壳组件
|
// 应用外壳组件
|
||||||
export { default as AppShell } from './AppShell.vue'
|
export { default as AppShell } from './AppShell.vue'
|
||||||
export { default as MobileNav } from './MobileNav.vue'
|
|
||||||
export { default as SidebarNav } from './SidebarNav.vue'
|
export { default as SidebarNav } from './SidebarNav.vue'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppShell
|
<AppShell
|
||||||
:show-notice="showAuthError"
|
:show-notice="showAuthError"
|
||||||
main-class=""
|
:main-class="mainClasses"
|
||||||
:sidebar-class="sidebarClasses"
|
:sidebar-class="sidebarClasses"
|
||||||
:content-class="contentClasses"
|
:content-class="contentClasses"
|
||||||
>
|
>
|
||||||
@@ -90,25 +90,156 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #header>
|
<template #header>
|
||||||
<!-- Mobile Header -->
|
<!-- Mobile Header (matches Home page style) -->
|
||||||
<div class="lg:hidden p-4 flex items-center justify-between border-b border-border bg-background/80 backdrop-blur-md">
|
<header class="lg:hidden fixed top-0 left-0 right-0 z-50 border-b border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/90 dark:bg-[#191714]/95 backdrop-blur-xl transition-all">
|
||||||
<RouterLink
|
<div class="mx-auto max-w-7xl px-6 py-4">
|
||||||
to="/"
|
<div class="flex items-center justify-between">
|
||||||
class="flex items-center gap-2"
|
<!-- Logo & Brand -->
|
||||||
|
<RouterLink
|
||||||
|
to="/"
|
||||||
|
class="flex items-center gap-3 group"
|
||||||
|
>
|
||||||
|
<HeaderLogo
|
||||||
|
size="h-9 w-9"
|
||||||
|
class-name="text-[#191919] dark:text-white"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col justify-center">
|
||||||
|
<h1 class="text-lg font-bold text-[#191919] dark:text-white leading-none">
|
||||||
|
Aether
|
||||||
|
</h1>
|
||||||
|
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1.5 font-medium tracking-wide">Multi Private Gateway</span>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<!-- Right Actions -->
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
||||||
|
:title="themeMode === 'system' ? '跟随系统' : themeMode === 'dark' ? '深色模式' : '浅色模式'"
|
||||||
|
@click="toggleDarkMode"
|
||||||
|
>
|
||||||
|
<SunMoon
|
||||||
|
v-if="themeMode === 'system'"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<SunMedium
|
||||||
|
v-else-if="themeMode === 'light'"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<Moon
|
||||||
|
v-else
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition"
|
||||||
|
@click="mobileMenuOpen = !mobileMenuOpen"
|
||||||
|
>
|
||||||
|
<div class="relative w-5 h-5">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="opacity-0 rotate-90 scale-75"
|
||||||
|
enter-to-class="opacity-100 rotate-0 scale-100"
|
||||||
|
leave-active-class="transition-all duration-150 ease-in absolute inset-0"
|
||||||
|
leave-from-class="opacity-100 rotate-0 scale-100"
|
||||||
|
leave-to-class="opacity-0 -rotate-90 scale-75"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
v-if="!mobileMenuOpen"
|
||||||
|
class="h-5 w-5"
|
||||||
|
/>
|
||||||
|
<X
|
||||||
|
v-else
|
||||||
|
class="h-5 w-5"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Dropdown Menu -->
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-300 ease-out overflow-hidden"
|
||||||
|
enter-from-class="opacity-0 max-h-0"
|
||||||
|
enter-to-class="opacity-100 max-h-[500px]"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
|
||||||
|
leave-from-class="opacity-100 max-h-[500px]"
|
||||||
|
leave-to-class="opacity-0 max-h-0"
|
||||||
>
|
>
|
||||||
<HeaderLogo
|
<div
|
||||||
size="h-8 w-8"
|
v-if="mobileMenuOpen"
|
||||||
class-name="text-[#191919] dark:text-white"
|
class="border-t border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)] bg-[#fafaf7]/95 dark:bg-[#191714]/98 backdrop-blur-xl"
|
||||||
/>
|
>
|
||||||
<span class="font-bold text-lg">Aether</span>
|
<div class="mx-auto max-w-7xl px-6 py-4">
|
||||||
</RouterLink>
|
<!-- Navigation Groups -->
|
||||||
<MobileNav
|
<div class="space-y-4">
|
||||||
:items="navigation"
|
<div
|
||||||
:is-active="isNavActive"
|
v-for="group in navigation"
|
||||||
:active-path="route.path"
|
:key="group.title"
|
||||||
:is-dark="isDark"
|
>
|
||||||
/>
|
<div
|
||||||
</div>
|
v-if="group.title"
|
||||||
|
class="text-[10px] font-semibold text-[#91918d] dark:text-muted-foreground uppercase tracking-wider mb-2"
|
||||||
|
>
|
||||||
|
{{ group.title }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<RouterLink
|
||||||
|
v-for="item in group.items"
|
||||||
|
:key="item.href"
|
||||||
|
:to="item.href"
|
||||||
|
class="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all"
|
||||||
|
:class="isNavActive(item.href)
|
||||||
|
? 'bg-[#cc785c]/10 dark:bg-[#cc785c]/20 text-[#cc785c] dark:text-[#d4a27f]'
|
||||||
|
: 'text-[#666663] dark:text-muted-foreground hover:bg-black/5 dark:hover:bg-white/5 hover:text-[#191919] dark:hover:text-white'"
|
||||||
|
@click="mobileMenuOpen = false"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
class="h-4 w-4 shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="truncate">{{ item.name }}</span>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Section -->
|
||||||
|
<div class="mt-4 pt-4 border-t border-[#cc785c]/10 dark:border-[rgba(227,224,211,0.12)]">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<div class="w-8 h-8 rounded-full bg-[#f0f0eb] dark:bg-white/10 border border-black/5 flex items-center justify-center text-xs font-bold text-[#3d3929] dark:text-[#d4a27f] shrink-0">
|
||||||
|
{{ authStore.user?.username?.substring(0, 2).toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col min-w-0">
|
||||||
|
<span class="text-sm font-semibold leading-none truncate text-[#191919] dark:text-white">{{ authStore.user?.username }}</span>
|
||||||
|
<span class="text-[10px] text-[#91918d] dark:text-muted-foreground leading-none mt-1">{{ authStore.user?.role === 'admin' ? '管理员' : '用户' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<RouterLink
|
||||||
|
to="/dashboard/settings"
|
||||||
|
class="p-2 hover:bg-muted/50 rounded-lg text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
@click="mobileMenuOpen = false"
|
||||||
|
>
|
||||||
|
<Settings class="w-4 h-4" />
|
||||||
|
</RouterLink>
|
||||||
|
<button
|
||||||
|
class="p-2 rounded-lg text-muted-foreground hover:text-red-500 transition-colors"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
<LogOut class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Desktop Page Header -->
|
<!-- Desktop Page Header -->
|
||||||
<header class="hidden lg:flex h-16 px-8 items-center justify-between shrink-0 border-b border-[#3d3929]/5 dark:border-white/5 sticky top-0 z-40 backdrop-blur-md bg-[#faf9f5]/90 dark:bg-[#191714]/90">
|
<header class="hidden lg:flex h-16 px-8 items-center justify-between shrink-0 border-b border-[#3d3929]/5 dark:border-white/5 sticky top-0 z-40 backdrop-blur-md bg-[#faf9f5]/90 dark:bg-[#191714]/90">
|
||||||
@@ -158,7 +289,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useDarkMode } from '@/composables/useDarkMode'
|
import { useDarkMode } from '@/composables/useDarkMode'
|
||||||
@@ -166,7 +297,6 @@ import { isDemoMode } from '@/config/demo'
|
|||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
import AppShell from '@/components/layout/AppShell.vue'
|
import AppShell from '@/components/layout/AppShell.vue'
|
||||||
import SidebarNav from '@/components/layout/SidebarNav.vue'
|
import SidebarNav from '@/components/layout/SidebarNav.vue'
|
||||||
import MobileNav from '@/components/layout/MobileNav.vue'
|
|
||||||
import HeaderLogo from '@/components/HeaderLogo.vue'
|
import HeaderLogo from '@/components/HeaderLogo.vue'
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
@@ -189,17 +319,25 @@ import {
|
|||||||
SunMoon,
|
SunMoon,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Megaphone,
|
Megaphone,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const { isDark, themeMode, toggleDarkMode } = useDarkMode()
|
const { themeMode, toggleDarkMode } = useDarkMode()
|
||||||
const isDemo = computed(() => isDemoMode())
|
const isDemo = computed(() => isDemoMode())
|
||||||
|
|
||||||
const showAuthError = ref(false)
|
const showAuthError = ref(false)
|
||||||
|
const mobileMenuOpen = ref(false)
|
||||||
let authCheckInterval: number | null = null
|
let authCheckInterval: number | null = null
|
||||||
|
|
||||||
|
// 路由变化时自动关闭移动端菜单
|
||||||
|
watch(() => route.path, () => {
|
||||||
|
mobileMenuOpen.value = false
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
authCheckInterval = setInterval(() => {
|
authCheckInterval = setInterval(() => {
|
||||||
if (authStore.user && !authStore.token) {
|
if (authStore.user && !authStore.token) {
|
||||||
@@ -330,6 +468,12 @@ const contentClasses = computed(() => {
|
|||||||
return `flex-1 min-w-0 bg-[#faf9f5] dark:bg-[#191714] text-[#3d3929] dark:text-[#d4a27f]`
|
return `flex-1 min-w-0 bg-[#faf9f5] dark:bg-[#191714] text-[#3d3929] dark:text-[#d4a27f]`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const mainClasses = computed(() => {
|
||||||
|
// 移动端需要 pt-24 来避开固定头部(约69px)+ 额外间距
|
||||||
|
// 桌面端内容在 sticky header 下方,但需要一些内边距让内容不紧贴
|
||||||
|
return `pt-24 lg:pt-6`
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user