Files
FileCodeBoxFronted/src/layout/AdminLayout/AdminLayout.vue
T
2026-06-03 10:43:34 +08:00

303 lines
7.2 KiB
Vue

<template>
<div
class="flex h-screen overflow-hidden flex-col transition-colors duration-300 lg:flex-row"
:class="[isDarkMode ? 'bg-gray-900' : 'bg-gray-50']"
>
<!-- Sidebar -->
<aside
class="fixed inset-y-0 border-transparent left-0 z-50 w-64 transform transition-all duration-300 ease-in-out lg:relative lg:translate-x-0 border-r flex flex-col h-full lg:h-screen"
:class="[
isDarkMode ? 'bg-gray-800 bg-opacity-90 backdrop-filter backdrop-blur-xl ' : 'bg-white ',
{ '-translate-x-full': !isSidebarOpen }
]"
>
<!-- Logo区域 -->
<div
class="flex items-center justify-between h-16 px-4 border-b"
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
>
<div class="flex items-center">
<div
class="rounded-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 p-1 animate-spin-slow"
>
<div class="rounded-full p-1" :class="[isDarkMode ? 'bg-gray-800' : 'bg-white']">
<BoxIcon
class="w-6 h-6"
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
/>
</div>
</div>
<h1
@click="router.push('/')"
class="ml-2 text-xl font-semibold cursor-pointer"
:class="[isDarkMode ? 'text-white' : 'text-gray-800']"
>
{{ t('common.appName') }}
</h1>
</div>
<button @click="toggleSidebar" class="lg:hidden">
<XIcon class="w-6 h-6" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']" />
</button>
</div>
<!-- 导航菜单 -->
<nav class="flex-1 overflow-y-auto custom-scrollbar">
<ul class="p-4 space-y-2">
<li v-for="item in menuItems" :key="item.id">
<RouterLink
:to="item.redirect"
class="flex items-center p-2 rounded-lg transition-colors duration-200"
:class="[
route.name === item.id
? isDarkMode
? 'bg-indigo-900 text-indigo-400'
: 'bg-indigo-100 text-indigo-600'
: isDarkMode
? 'text-gray-400 hover:bg-gray-700'
: 'text-gray-600 hover:bg-gray-100'
]"
>
<component :is="item.icon" class="w-5 h-5 mr-3" />
{{ item.name }}
</RouterLink>
</li>
</ul>
</nav>
<!-- 退出登录按钮 -->
<div class="p-4 border-t" :class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']">
<button
@click="handleLogout"
class="flex items-center w-full p-2 rounded-lg transition-colors duration-200"
:class="[
isDarkMode
? 'text-gray-400 hover:bg-gray-700 hover:text-white'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
]"
>
<LogOutIcon class="w-5 h-5 mr-3" />
{{ t('admin.logout') }}
</button>
</div>
</aside>
<!-- Main Content -->
<div class="flex h-full min-h-0 flex-1 flex-col">
<!-- Header -->
<header
class="shadow-md border-b transition-colors duration-300 h-16"
:class="[isDarkMode ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200']"
>
<div class="flex items-center justify-between h-16 px-4">
<button @click="toggleSidebar" class="lg:hidden">
<MenuIcon class="w-6 h-6" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']" />
</button>
</div>
</header>
<!-- Content -->
<main
class="min-h-0 flex-1 overflow-y-auto transition-colors duration-300 custom-scrollbar"
:class="[isDarkMode ? 'bg-gray-900' : 'bg-gray-50']"
>
<router-view />
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, inject, onMounted, onUnmounted } from 'vue'
import {
BoxIcon,
MenuIcon,
XIcon,
FolderIcon,
CogIcon,
LayoutDashboardIcon,
LogOutIcon
} from 'lucide-vue-next'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ROUTE_NAMES, ROUTES } from '@/constants'
import { useAdminSession } from '@/composables'
interface MenuItem {
id: string
name: string
icon: typeof LayoutDashboardIcon
redirect: string
}
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const isDarkMode = inject('isDarkMode')
const { verifySession, logout } = useAdminSession()
const menuItems: MenuItem[] = [
{
id: ROUTE_NAMES.DASHBOARD,
name: t('admin.dashboard.title'),
icon: LayoutDashboardIcon,
redirect: ROUTES.DASHBOARD
},
{
id: ROUTE_NAMES.FILE_MANAGE,
name: t('admin.fileManage.title'),
icon: FolderIcon,
redirect: ROUTES.FILE_MANAGE
},
{
id: ROUTE_NAMES.SETTINGS,
name: t('admin.settings.title'),
icon: CogIcon,
redirect: ROUTES.SETTINGS
}
]
const isSidebarOpen = ref(true)
const toggleSidebar = () => {
isSidebarOpen.value = !isSidebarOpen.value
}
// 响应式处理
const handleResize = () => {
if (window.innerWidth >= 1024) {
isSidebarOpen.value = true
} else {
isSidebarOpen.value = false
}
}
onMounted(() => {
handleResize()
window.addEventListener('resize', handleResize)
void verifySession().then((isValid) => {
if (!isValid) {
void router.push(ROUTES.LOGIN)
}
})
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
// 登出处理
const handleLogout = async () => {
await logout()
await router.push(ROUTES.LOGIN)
}
</script>
<style>
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #e5e7eb;
transition: 0.4s;
}
.dark .slider {
background-color: #4b5563;
}
input:checked + .slider {
background-color: #4f46e5;
}
.dark input:checked + .slider {
background-color: #4f46e5;
}
.slider:before {
position: absolute;
content: '';
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
}
.dark .slider:before {
background-color: #e5e7eb;
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin-slow {
animation: spin 8s linear infinite;
}
.transition-colors {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.custom-scrollbar {
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: #cbd5e0;
border-radius: 3px;
&:hover {
background-color: #a0aec0;
}
}
}
/* 暗黑模式下的滚动条样式 */
:global(.dark) .custom-scrollbar {
&::-webkit-scrollbar-thumb {
background-color: #4a5568;
&:hover {
background-color: #2d3748;
}
}
}
</style>