refactor(layout): 重构应用布局结构
- 将原有布局中的侧边栏和头部组件拆分为独立的 AppSidebar 和 AppHeader 组件 - 移除内联的导航逻辑和样式,交由专用组件管理 - 更新图标库,优化部分图标的显示效果 - 简化认证存储逻辑,增强状态持久化与安全性 - 优化侧边栏状态管理机制,提高响应式体验 - 改进登录流程错误处理,增加网络异常提示 - 调整路由组件结构,提升代码可维护性
This commit is contained in:
60
src/lib/components/layout/app/AppHeader.svelte
Normal file
60
src/lib/components/layout/app/AppHeader.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
import Icon from '$lib/components/icon/Icon.svelte';
|
||||
import ThemeSelector from '$lib/widget/ThemeSelector.svelte';
|
||||
import { authStore } from '$lib/stores/authStore.ts';
|
||||
import { toggleSidebar } from '$lib/stores/sidebarStore';
|
||||
</script>
|
||||
|
||||
<header class="w-full h-18 flex justify-between items-center px-4 bg-base-300 flex-shrink-0">
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-square btn-ghost"
|
||||
onclick={toggleSidebar}
|
||||
aria-label="Toggle Sidebar"
|
||||
>
|
||||
<Icon id="menu" size="24" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center items-center gap-4 select-none">
|
||||
<ThemeSelector/>
|
||||
|
||||
{#if $authStore.user }
|
||||
<div class="dropdown dropdown-end ">
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="rounded-full bg-primary h-8 w-8 p-4 flex items-center justify-center text-primary-content font-bold"
|
||||
>
|
||||
{#if $authStore.user.avatar}
|
||||
<img
|
||||
class="w-8 h-8 rounded-full"
|
||||
src="{$authStore.user.avatar}"
|
||||
alt="Avatar"
|
||||
/>
|
||||
{:else}
|
||||
<span>{$authStore.user.nickname.slice(0, 1)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="dropdown-content mt-2 w-64 shadow-base-300 p-12 shadow-2xl bg-base-200 border border-base-content/10 rounded-box ">
|
||||
<div class="text-center ">
|
||||
<p class="font-bold">{$authStore.user.nickname}</p>
|
||||
<p class="text-xs mt-2">{$authStore.user.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<button class="btn btn-primary btn-sm" onclick={() => goto(resolve("/auth/login"))}>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
213
src/lib/components/layout/app/AppSidebar.svelte
Normal file
213
src/lib/components/layout/app/AppSidebar.svelte
Normal file
@@ -0,0 +1,213 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { fly, fade } from 'svelte/transition';
|
||||
|
||||
import Icon from '$lib/components/icon/Icon.svelte';
|
||||
import { sidebarStore, setSidebarOpen } from '$lib/stores/sidebarStore';
|
||||
import { authStore } from '$lib/stores/authStore.ts'; // 假设你有这个store
|
||||
import { } from '$lib/stores/authStore.ts'; // 假设你有这个store
|
||||
import type { NavItem } from '$lib/types/layout.ts';
|
||||
import { authService } from '$lib/api/services/authService.ts';
|
||||
|
||||
// 模拟一些数据,增加了分组的概念(如果需要)
|
||||
const rawNavItems: NavItem[] = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: '仪表盘',
|
||||
icon: 'home',
|
||||
href: '/app/dashboard',
|
||||
},
|
||||
{
|
||||
id: 'statistics',
|
||||
label: '数据看板',
|
||||
icon: 'data-pie',
|
||||
href: '/app/statistics',
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: '系统设置',
|
||||
icon: 'settings',
|
||||
href: '/app/settings',
|
||||
subItems: [
|
||||
{
|
||||
id: 'users',
|
||||
label: '用户管理',
|
||||
href: '/app/settings/users',
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
label: '角色权限',
|
||||
href: '/app/settings/roles',
|
||||
},
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 递归计算高亮状态
|
||||
* 只要子元素有一个是激活的,父元素就算激活
|
||||
*/
|
||||
function processNavItems(items: NavItem[], currentPath: string): any[] {
|
||||
return items.map(item => {
|
||||
// 判断当前项是否匹配
|
||||
const isSelfActive = item.href === '/'
|
||||
? currentPath === '/'
|
||||
: currentPath.startsWith(item.href);
|
||||
|
||||
// 处理子项
|
||||
let processedSubItems = undefined;
|
||||
let isChildActive = false;
|
||||
|
||||
if (item.subItems) {
|
||||
processedSubItems = processNavItems(item.subItems, currentPath);
|
||||
// 检查是否有子项被激活
|
||||
isChildActive = processedSubItems.some(sub => sub.isActive || sub.isChildActive);
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
subItems: processedSubItems,
|
||||
isActive: isSelfActive, // 自身路径匹配
|
||||
isChildActive: isChildActive // 子路径匹配(用于展开菜单)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 使用 $derived 动态计算
|
||||
let navItems = $derived(processNavItems(rawNavItems, page.url.pathname));
|
||||
|
||||
function handleMobileClose() {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
if (isMobile && $sidebarStore.isOpen) {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#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 w-64"
|
||||
>
|
||||
|
||||
<div class="h-18 p-4 flex items-center flex-shrink-0">
|
||||
<a href={resolve("/app/dashboard")} class="flex items-center gap-3" onclick={handleMobileClose}>
|
||||
<Icon id="logo" size="32" className="flex-shrink-0" />
|
||||
<p class="truncate font-bold font-serif text-lg">IT DTMS</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<ul class="menu px-2 menu-vertical w-full gap-1">
|
||||
{#each navItems as item (item.id)}
|
||||
|
||||
{#if item.subItems && item.subItems.length > 0}
|
||||
<li>
|
||||
<details open={item.isChildActive}>
|
||||
<summary class="group {item.isActive ? 'text-primary font-medium' : ''}">
|
||||
<Icon id={item.icon} size="20" />
|
||||
<span class="truncate">{item.label}</span>
|
||||
</summary>
|
||||
<ul>
|
||||
{#each item.subItems as subItem (subItem.id)}
|
||||
<li>
|
||||
<a
|
||||
href={resolve(subItem.href)}
|
||||
onclick={handleMobileClose}
|
||||
class={subItem.isActive ? 'active font-medium' : ''}
|
||||
>
|
||||
<span class="text-xs opacity-50">•</span>
|
||||
<span class="truncate">{subItem.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
{:else}
|
||||
<li>
|
||||
<a
|
||||
href={resolve(item.href)}
|
||||
onclick={handleMobileClose}
|
||||
class="group {item.isActive ? 'active font-medium' : ''}"
|
||||
>
|
||||
<Icon id={item.icon} size="20" />
|
||||
<span class="truncate">{item.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{#if $authStore.isAuthenticated && $authStore.user}
|
||||
<div class="p-3 border-t border-base-content/10 flex-shrink-0 bg-base-200/50">
|
||||
<div class="dropdown dropdown-top dropdown-end w-full">
|
||||
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="flex items-center gap-3 p-2 rounded-lg hover:bg-base-300 transition-colors cursor-pointer w-full"
|
||||
>
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-10">
|
||||
{#if $authStore.user?.avatar}
|
||||
<img src={$authStore.user.avatar} alt="avatar" />
|
||||
{:else}
|
||||
<span class="text-xs">User</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<span class="text-sm font-bold truncate">{$authStore.user.nickname}</span>
|
||||
<span class="text-xs text-base-content/60 truncate">@{$authStore.user?.username}</span>
|
||||
</div>
|
||||
|
||||
<Icon id="chevrons-up-down" size="16" className="opacity-50" />
|
||||
</div>
|
||||
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow-lg bg-base-100 rounded-box w-60 mb-2 border border-base-content/10">
|
||||
<li class="menu-title px-4 py-2">我的账户</li>
|
||||
<li><a href="/app/profile"><Icon id="user-profile" size="16"/> 个人资料</a></li>
|
||||
<li><a href="/app/settings"><Icon id="settings" size="16"/> 设置</a></li>
|
||||
<div class="divider my-1"></div>
|
||||
<li>
|
||||
<button class="text-error" onclick={authService.logout}>
|
||||
<Icon id="sign-out" size="16"/> 退出登录
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* 可选:美化滚动条 */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(156, 163, 175, 0.3);
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user