2025-12-04 10:04:21 +08:00

159 lines
4.0 KiB
Vue

<template>
<div class="admin-layout">
<aside class="admin-layout__sidebar">
<div class="admin-layout__brand">
<div class="brand-title">控制台</div>
<div class="brand-sub">Admin Console</div>
</div>
<nav class="admin-layout__nav">
<NuxtLink
v-for="item in nav"
:key="item.to"
:to="item.to"
class="nav-item"
:class="{ active: isActive(item.to) }"
>
{{ item.label }}
</NuxtLink>
</nav>
</aside>
<main class="admin-layout__main">
<header class="admin-layout__topbar">
<div>
<div class="topbar-title">{{ pageTitle }}</div>
<div class="topbar-sub">{{ pageSub }}</div>
</div>
<NuxtLink to="/" class="home-link">返回前台</NuxtLink>
</header>
<div class="admin-layout__content">
<slot />
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const nav = [
{ label: '仪表盘', to: '/admin' },
{ label: '用户管理', to: '/admin/users' },
{ label: '角色管理', to: '/admin/roles' },
{ label: '文章管理', to: '/admin/articles' },
{ label: '菜单标签', to: '/admin/menu-tags' },
{ label: '首页推送', to: '/admin/home-featured' },
]
const currentTitle = computed(() => {
const hit = nav.find((item) => route.path.startsWith(item.to))
return hit?.label ?? '后台管理'
})
const isActive = (path: string): boolean => {
if (path === '/admin') return route.path === '/admin'
return route.path.startsWith(path)
}
const pageTitle = computed(() => {
const metaTitle = (route.meta as any)?.adminTitle as string | undefined
return metaTitle || currentTitle.value
})
const pageSub = computed(() => {
const metaSub = (route.meta as any)?.adminSub as string | undefined
return metaSub || '站点后台 · 管理用户、角色与文章'
})
</script>
<style scoped>
.admin-layout {
min-height: 100vh;
display: grid;
grid-template-columns: 240px 1fr;
background: #f5f7fb;
}
.admin-layout__sidebar {
background: #0f172a;
color: #e5e7eb;
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 16px;
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.08);
}
.admin-layout__brand { margin-bottom: 6px; }
.brand-title { font-size: 18px; font-weight: 800; }
.brand-sub { font-size: 12px; color: #cbd5e1; margin-top: 2px; }
.admin-layout__nav { display: flex; flex-direction: column; gap: 10px; }
.nav-item {
display: block;
padding: 10px 12px;
border-radius: 12px;
color: #e5e7eb;
text-decoration: none;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
transition: all 0.2s ease;
}
.nav-item:hover { background: rgba(255, 255, 255, 0.12); }
.nav-item.active {
background: linear-gradient(120deg, #22d3ee, #6366f1);
color: #0b1224;
border-color: transparent;
font-weight: 700;
box-shadow: 0 8px 18px rgba(99, 102, 241, 0.35);
}
.admin-layout__main {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.admin-layout__topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.topbar-title { font-size: 20px; font-weight: 800; }
.topbar-sub { color: #6b7280; margin-top: 2px; font-size: 13px; }
.home-link {
padding: 8px 12px;
border-radius: 10px;
border: 1px solid #e5e7eb;
text-decoration: none;
color: #111827;
background: #fff;
transition: all 0.2s ease;
}
.home-link:hover { border-color: #111827; background: #f9fafb; }
.admin-layout__content {
padding: 18px clamp(16px, 5vw, 36px);
flex: 1 1 auto;
min-height: 0;
}
@media (max-width: 1024px) {
.admin-layout { grid-template-columns: 1fr; }
.admin-layout__sidebar {
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.admin-layout__nav { flex-direction: row; flex-wrap: wrap; gap: 8px; }
.nav-item { flex: 1 1 140px; text-align: center; }
}
</style>