feat: 实现后台管理的基础布局和认证钩子逻辑

This commit is contained in:
seahi committed 2026-07-26 17:52:39 +08:00
1 parent 8fb1f7da94
commit 69e01bdc32
4 files changed
+125 -1

No files matched your search

+5 -1
View File
@@ -12,7 +12,11 @@ declare global {
}
// interface Error {}
// interface Locals {}
interface Locals {
user?: {
email: string;
}
}
// interface PageData {}
// interface PageState {}
}
+24
View File
@@ -0,0 +1,24 @@
import type { Handle } from '@sveltejs/kit';
import { dev } from '$app/environment';
export const handle: Handle = async ({ event, resolve }) => {
// 拦截 /admin 路由
if (event.url.pathname.startsWith('/admin')) {
// 读取 Cloudflare Access 提供的 Header
const userEmail = event.request.headers.get('cf-access-authenticated-user-email');
// 本地开发环境通过 $app/environment 的 dev 变量来判断
const isLocalDev = dev;
if (!userEmail && !isLocalDev) {
return new Response('Unauthorized', { status: 401 });
}
// 将用户信息存入 locals,供后续使用
event.locals.user = {
email: userEmail || 'dev@local.host'
};
}
return resolve(event);
};
+7
View File
@@ -0,0 +1,7 @@
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
return {
user: locals.user
};
};
+89
View File
@@ -0,0 +1,89 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
let { children } = $props();
let scrolled = $state(false);
onMount(() => {
const handleScroll = () => {
scrolled = window.scrollY > 50;
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
});
</script>
<svelte:head>
<title>后台管理 - BlogFlare</title>
</svelte:head>
<header class="header {scrolled ? 'scrolled' : ''}">
<div class="container wide">
<a href="/admin" class="brand">BlogFlare Admin</a>
<nav>
<a href="/admin" class={$page.url.pathname === '/admin' ? 'active' : ''}>仪表盘</a>
<a href="/admin/posts/new" class={$page.url.pathname === '/admin/posts/new' ? 'active' : ''}>撰写</a>
<div class="nav-dropdown">
<span class="nav-dropdown-trigger {($page.url.pathname.startsWith('/admin/posts') && $page.url.pathname !== '/admin/posts/new') || $page.url.pathname.startsWith('/admin/categories') || $page.url.pathname.startsWith('/admin/tags') ? 'active' : ''}">
管理
</span>
<div class="dropdown-menu">
<a href="/admin/posts">文章</a>
<a href="/admin/categories">分类</a>
<a href="/admin/tags">标签</a>
</div>
</div>
<a href="/admin/settings" class={$page.url.pathname.startsWith('/admin/settings') ? 'active' : ''}>设置</a>
</nav>
<div class="user-info">
<a href="/" target="_blank" class="site-link" title="访问前台">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
<span>站点首页</span>
</a>
<span class="user-email">{$page.data.user?.email || 'Admin'}</span>
</div>
</div>
</header>
<main id="pjax-container" style="padding-top: 58px; min-height: calc(100vh - 120px);">
<div class="wrapper container wide admin-container">
{@render children()}
</div>
</main>
<style>
.admin-container {
padding-top: 2rem;
padding-bottom: 4rem;
}
.user-info {
position: absolute;
right: 20px;
display: flex;
align-items: center;
gap: 15px;
font-size: 0.9rem;
color: var(--header-text);
}
.site-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--header-text);
text-decoration: none;
font-size: 0.85rem;
opacity: 0.85;
transition: opacity 0.2s;
}
.site-link:hover {
opacity: 1;
}
</style>