feat(layout): 实现应用布局和侧边栏功能

- 添加侧边栏组件,支持展开/收缩和移动端适配
- 实现导航菜单,支持高亮当前路由
- 添加主题选择器组件
- 集成认证状态显示和登出功能
- 优化侧边栏在不同屏幕尺寸下的行为
- 添加多种图标支持,包括logo、菜单、主页等
- 创建NavItem类型定义,用于导航菜单项
- 扩展sidebarStore,增加手动控制状态管理
- 添加数据看板页面占位内容
- 更新全局布局文件以支持主题和侧边栏状态管理
This commit is contained in:
Chaos
2025-11-22 22:45:49 +08:00
parent 26fef2fd7a
commit 65cf80fb51
9 changed files with 325 additions and 81 deletions

View File

@@ -1,8 +1,154 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { authService } from '$lib/api/services/authService.ts';
import ThemeSelector from '$lib/widget/ThemeSelector.svelte';
import Icon from '$lib/components/icon/Icon.svelte';
import { authStore } from '$lib/stores/authStore.ts';
import { sidebarStore, toggleSidebar, toggleSidebarOpen } from '$lib/stores/sidebarStore';
import { page } from '$app/state';
import { fly, fade } from 'svelte/transition';
import type { NavItem } from '$lib/types/layout.ts';
let { children } = $props();
const rawNavItems: NavItem[] = [
{
id: 'dashboard',
label: '仪表盘',
icon: 'home',
href: '/app/dashboard',
isActive: true
},
{
id: 'statistics',
label: '数据看板',
icon: 'data-pie',
href: '/app/statistics',
}
];
/**
* 处理移动端关闭逻辑
* 仅在屏幕宽度小于 768px (Tailwind 的 md 断点) 时触发关闭
*/
function handleMobileClose() {
// 获取当前窗口宽度
const isMobile = window.innerWidth < 768;
// 如果是移动端且侧边栏是打开状态,则关闭它
if (isMobile && $sidebarStore.isOpen) {
toggleSidebarOpen();
}
}
// 2. 使用 $derived 生成响应式的新数组
// Svelte 5 会自动追踪 $page.url.pathname 的变化
let navItems = $derived(rawNavItems.map(item => {
// 处理根路径 '/' 的特殊逻辑,防止所有页面都高亮首页
const isActive = item.href === '/'
? page.url.pathname === '/'
: page.url.pathname.startsWith(item.href);
return {
...item,
isActive // 将计算结果合并进去
};
}));
</script>
<main class="">
{@render children()}
</main>
<div class="flex h-screen bg-base-300 overflow-hidden relative">
{#if $sidebarStore.isOpen}
<div
role="button"
tabindex="0"
class="fixed inset-0 bg-black/50 z-20 md:hidden cursor-pointer backdrop-blur-sm"
onclick={handleMobileClose}
onkeydown={(e) => e.key === 'Escape' && handleMobileClose()}
transition:fade={{ duration: 200 }}
></div>
<aside
in:fly={{duration: 200, x: -100}}
out:fly={{duration: 200, x: -100}}
class="flex-shrink-0 flex flex-col bg-base-200 border-r border-base-100/70 fixed h-full md:relative z-30
transition-all duration-500 ease-in-out {$sidebarStore.isExpanded ? 'w-56' : 'w-16'}"
>
<div class="h-12 p-4 transition-all duration-1000">
<a href={resolve("/app/dashboard")} class="flex items-center gap-2" onclick={handleMobileClose}>
<Icon id="logo" size="32" className="flex-shrink-0" />
<p class="truncate flex-shrink-1"> </p>
{#if $sidebarStore.isExpanded}
<p class="truncate font-bold font-serif">IT DTMS</p>
{/if}
</a>
</div>
<ul class="menu menu-vertical transition-all duration-1000 w-full">
{#each navItems as item(item.id)}
<li class="w-full {item.isActive ? 'menu-active' : ''}">
<a href={resolve(item.href)} onclick={handleMobileClose}>
<Icon id={item.icon} size="24" />
{#if $sidebarStore.isExpanded}
<p class="truncate">{item.label}</p>
{/if}
</a>
</li>
{/each}
</ul>
<div class="absolute bottom-2 right-3">
<button
class="btn btn-square btn-ghost"
onclick={toggleSidebar}
>
<Icon id="menu" size="24" />
</button>
</div>
</aside>
{/if}
<div class="w-full overflow-y-auto">
<header class="w-full h-18 flex justify-between items-center px-4 bg-base-300">
<div class="md:hidden">
<button
class="btn btn-square btn-ghost"
onclick={toggleSidebarOpen}
>
<Icon id="menu" size="24" />
</button>
</div>
<div class=""></div>
<div class="flex justify-center items-center gap-4">
<ThemeSelector/>
{#if $authStore.isAuthenticated}
<button
tabindex="0"
class="rounded-full bg-primary h-12 w-12"
onclick={()=>{
console.log("退出登录")
authService.logout()
}}
aria-label="Logout"
></button>
{:else}
<div class="flex items-center">
<div class="w-24">
<button class="btn btn-primary btn-wide" onclick={()=>{goto(resolve("/auth/login"))}}>登录</button>
</div>
</div>
{/if}
</div>
</header>
<main class="">
{@render children()}
</main>
</div>
</div>