refactor(layout): 重构应用布局结构
- 将原有布局中的侧边栏和头部组件拆分为独立的 AppSidebar 和 AppHeader 组件 - 移除内联的导航逻辑和样式,交由专用组件管理 - 更新图标库,优化部分图标的显示效果 - 简化认证存储逻辑,增强状态持久化与安全性 - 优化侧边栏状态管理机制,提高响应式体验 - 改进登录流程错误处理,增加网络异常提示 - 调整路由组件结构,提升代码可维护性
This commit is contained in:
@@ -1,48 +1,62 @@
|
|||||||
|
import { api } from '$lib/api/httpClient'; // 通常不需要 .ts 后缀
|
||||||
import {api} from '$lib/api/httpClient.ts'
|
import type { AuthResponse, LoginPayload } from '$lib/types/auth';
|
||||||
import type { AuthResponse, LoginPayload } from '$lib/types/auth.ts';
|
import { authStore } from '$lib/stores/authStore';
|
||||||
import { browser } from '$app/environment';
|
import { userService } from '$lib/api/services/userService';
|
||||||
import { authStore } from '$lib/stores/authStore.ts';
|
import { toast } from '$lib/stores/toastStore';
|
||||||
import { userService } from '$lib/api/services/userService.ts';
|
import { get } from 'svelte/store';
|
||||||
import { userStore } from '$lib/stores/userStore.ts';
|
|
||||||
|
|
||||||
export const authService = {
|
export const authService = {
|
||||||
|
/**
|
||||||
|
* 登录流程
|
||||||
|
*/
|
||||||
login: async (payload: LoginPayload): Promise<AuthResponse> => {
|
login: async (payload: LoginPayload): Promise<AuthResponse> => {
|
||||||
|
// 1. 调用登录接口
|
||||||
const response = await api.post<AuthResponse>('/auth/login', payload);
|
const response = await api.post<AuthResponse>('/auth/login', payload);
|
||||||
|
|
||||||
if (response.code != 200 || !response.data){
|
if (response.code !== 200 || !response.data) {
|
||||||
throw new Error(response.msg);
|
throw new Error(response.msg || '登录失败');
|
||||||
}
|
|
||||||
if (browser){
|
|
||||||
authService._setToken(response.data.token, response.data.tokenHead)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { token, tokenHead } = response.data;
|
||||||
|
|
||||||
|
// 2. 临时设置 Token 到 Store
|
||||||
|
// 这一步是必须的,因为接下来的 userService.getUserProfile()
|
||||||
|
// 里的 API 请求拦截器需要读取 Store 中的 Token 才能通过鉴权。
|
||||||
|
// 我们先以“部分登录”的状态更新 Store。
|
||||||
|
authStore.update(s => ({ ...s, token, tokenHead, isAuthenticated: true }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. 获取用户信息
|
||||||
const userProfile = await userService.getUserProfile();
|
const userProfile = await userService.getUserProfile();
|
||||||
|
|
||||||
if (browser){
|
// 4. 最终确认登录状态(更新完整信息并持久化)
|
||||||
userStore.set(userProfile)
|
// 这里调用 Store 封装好的 login 方法,它会负责写入 localStorage
|
||||||
}
|
authStore.login({
|
||||||
|
token,
|
||||||
|
tokenHead,
|
||||||
|
user: userProfile
|
||||||
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 5. 安全回滚
|
||||||
|
// 如果获取用户信息失败(比如 Token 虽然返回了但无效,或者网络波动),
|
||||||
|
// 我们应该立即清除刚才设置的临时 Token,防止应用处于中间状态。
|
||||||
|
console.error('获取用户信息失败,回滚登录状态', error);
|
||||||
|
authStore.logout();
|
||||||
|
throw error; // 继续抛出错误给 UI 层处理
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出流程
|
||||||
|
*/
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
|
// 逻辑大大简化:只负责调用 Store 和 UI 反馈
|
||||||
|
authStore.logout();
|
||||||
|
toast.success('退出登录成功');
|
||||||
|
|
||||||
if (browser){
|
// 如果需要调用后端登出接口(使 Token 失效),在这里 await api.post('/auth/logout')
|
||||||
|
|
||||||
authStore.clear();
|
|
||||||
userStore.clear();
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
localStorage.removeItem('auth_token_head');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
_setToken: (token:string ,tokenHead: string)=> {
|
|
||||||
authStore.set({ token, tokenHead, isAuthenticated: true });
|
|
||||||
localStorage.setItem('auth_token', token);
|
|
||||||
localStorage.setItem('auth_token_head', tokenHead);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,43 +44,12 @@
|
|||||||
</g>
|
</g>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="data-pie" viewBox="0 0 32 32">
|
<symbol id="data-pie" viewBox="0 0 16 16">
|
||||||
<g fill="none">
|
<path fill="currentColor" d="M14 4.5A2.5 2.5 0 0 0 11.5 2h-7A2.5 2.5 0 0 0 2 4.5v7A2.5 2.5 0 0 0 4.5 14H7v-1H4.5A1.5 1.5 0 0 1 3 11.5V6h10v.268A2 2 0 0 1 14 8zM4.5 3h7A1.5 1.5 0 0 1 13 4.5V5H3v-.5A1.5 1.5 0 0 1 4.5 3M12 7a1 1 0 0 0-1 1v7a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1m-3 4a1 1 0 0 0-1 1v3a1 1 0 1 0 2 0v-3a1 1 0 0 0-1-1m5-1a1 1 0 1 1 2 0v5a1 1 0 1 1-2 0z" />
|
||||||
<path fill="url(#SVGkSGRmbrV)" d="M15 7.012a1 1 0 0 0-1.047-1C7.855 6.3 3 11.333 3 17.5C3 23.851 8.149 29 14.5 29c6.168 0 11.201-4.855 11.487-10.953A1 1 0 0 0 24.988 17H17.5a2.5 2.5 0 0 1-2.5-2.5z" />
|
|
||||||
<path fill="url(#SVGOonY3dQP)" d="M18.047 3.013A1 1 0 0 0 17 4.012V14a1 1 0 0 0 1 1h9.988a1 1 0 0 0 1-1.047C28.71 8.037 23.962 3.29 18.046 3.013" />
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="SVGkSGRmbrV" x1="25.988" x2="-10.793" y1="29" y2="-7.781" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#6d37cd" />
|
|
||||||
<stop offset=".641" stop-color="#ea71ef" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="SVGOonY3dQP" x1="27.989" x2="19.398" y1="12.802" y2="3.012" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#e23cb4" />
|
|
||||||
<stop offset="1" stop-color="#ea71ef" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</g>
|
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="home" viewBox="0 0 48 48">
|
<symbol id="home" viewBox="0 0 20 20">
|
||||||
<g fill="none">
|
<path fill="currentColor" d="M8.998 2.388a1.5 1.5 0 0 1 2.005 0l5.5 4.942A1.5 1.5 0 0 1 17 8.445V15.5a1.5 1.5 0 0 1-1.5 1.5H13a1.5 1.5 0 0 1-1.5-1.5V12a.5.5 0 0 0-.5-.5H9a.5.5 0 0 0-.5.5v3.5A1.5 1.5 0 0 1 7 17H4.5A1.5 1.5 0 0 1 3 15.5V8.445c0-.425.18-.83.498-1.115zm1.336.744a.5.5 0 0 0-.668 0l-5.5 4.942A.5.5 0 0 0 4 8.445V15.5a.5.5 0 0 0 .5.5H7a.5.5 0 0 0 .5-.5V12A1.5 1.5 0 0 1 9 10.5h2a1.5 1.5 0 0 1 1.5 1.5v3.5a.5.5 0 0 0 .5.5h2.5a.5.5 0 0 0 .5-.5V8.445a.5.5 0 0 0-.166-.371z" />
|
||||||
<path fill="url(#SVGHYC0XdPj)" d="M18.067 27h12v16h-12z" />
|
|
||||||
<path fill="url(#SVGAip0Sdul)" d="M26.461 7.855a3.78 3.78 0 0 0-4.787 0L8.499 18.597a3.91 3.91 0 0 0-1.432 3.031v17.485C7.067 41.26 8.78 43 10.892 43h8.175V30.5a2.5 2.5 0 0 1 2.5-2.5h5a2.5 2.5 0 0 1 2.5 2.5V43h8.175c2.113 0 3.825-1.74 3.825-3.887V21.628a3.91 3.91 0 0 0-1.43-3.031z" />
|
|
||||||
<path fill="url(#SVGDweprdys)" fill-rule="evenodd" d="m24.068 9.329l-16 13.215a2.054 2.054 0 0 1-2.852-.262a1.956 1.956 0 0 1 .267-2.794L22.28 5.628a2.83 2.83 0 0 1 3.523-.024l16.805 13.588a1.957 1.957 0 0 1 .307 2.79a2.054 2.054 0 0 1-2.848.3z" clip-rule="evenodd" />
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="SVGHYC0XdPj" x1="24.067" x2="13.481" y1="27" y2="44.65" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#944600" />
|
|
||||||
<stop offset="1" stop-color="#cd8e02" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="SVGAip0Sdul" x1="10.313" x2="45.173" y1="5.24" y2="32" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#ffd394" />
|
|
||||||
<stop offset="1" stop-color="#ffb357" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="SVGDweprdys" x1="17.817" x2="25.308" y1=".725" y2="22.452" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#ff921f" />
|
|
||||||
<stop offset="1" stop-color="#eb4824" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</g>
|
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol id="menu" viewBox="0 0 24 24">
|
<symbol id="menu" viewBox="0 0 24 24">
|
||||||
@@ -137,4 +106,21 @@
|
|||||||
<path fill="#EF4444" d="m13.299 3.148l8.634 14.954a1.5 1.5 0 0 1-1.299 2.25H3.366a1.5 1.5 0 0 1-1.299-2.25l8.634-14.954c.577-1 2.02-1 2.598 0" class="duoicon-secondary-layer" opacity="0.3" />
|
<path fill="#EF4444" d="m13.299 3.148l8.634 14.954a1.5 1.5 0 0 1-1.299 2.25H3.366a1.5 1.5 0 0 1-1.299-2.25l8.634-14.954c.577-1 2.02-1 2.598 0" class="duoicon-secondary-layer" opacity="0.3" />
|
||||||
<path fill="#EF4444" d="M12 8a1 1 0 0 0-.993.883L11 9v4a1 1 0 0 0 1.993.117L13 13V9a1 1 0 0 0-1-1m0 7a1 1 0 1 0 0 2a1 1 0 0 0 0-2" class="duoicon-primary-layer" />
|
<path fill="#EF4444" d="M12 8a1 1 0 0 0-.993.883L11 9v4a1 1 0 0 0 1.993.117L13 13V9a1 1 0 0 0-1-1m0 7a1 1 0 1 0 0 2a1 1 0 0 0 0-2" class="duoicon-primary-layer" />
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="settings" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" d="M8 6a2 2 0 1 0 0 4a2 2 0 0 0 0-4M7 8a1 1 0 1 1 2 0a1 1 0 0 1-2 0m3.618-3.602a.71.71 0 0 1-.824-.567l-.26-1.416a.35.35 0 0 0-.275-.282a6.1 6.1 0 0 0-2.519 0a.35.35 0 0 0-.275.282l-.259 1.416a.71.71 0 0 1-.936.538l-1.359-.484a.36.36 0 0 0-.382.095a6 6 0 0 0-1.262 2.173a.35.35 0 0 0 .108.378l1.102.931q.045.037.081.081a.704.704 0 0 1-.081.995l-1.102.931a.35.35 0 0 0-.108.378A6 6 0 0 0 3.53 12.02a.36.36 0 0 0 .382.095l1.36-.484a.708.708 0 0 1 .936.538l.258 1.416c.026.14.135.252.275.281a6.1 6.1 0 0 0 2.52 0a.35.35 0 0 0 .274-.281l.26-1.416a.71.71 0 0 1 .936-.538l1.359.484c.135.048.286.01.382-.095a6 6 0 0 0 1.262-2.173a.35.35 0 0 0-.108-.378l-1.102-.931a.703.703 0 0 1 0-1.076l1.102-.931a.35.35 0 0 0 .108-.378A6 6 0 0 0 12.47 3.98a.36.36 0 0 0-.382-.095l-1.36.484a1 1 0 0 1-.111.03m-6.62.58l.937.333a1.71 1.71 0 0 0 2.255-1.3l.177-.97a5 5 0 0 1 1.265 0l.178.97a1.708 1.708 0 0 0 2.255 1.3L12 4.977q.384.503.63 1.084l-.754.637a1.704 1.704 0 0 0 0 2.604l.755.637a5 5 0 0 1-.63 1.084l-.937-.334a1.71 1.71 0 0 0-2.255 1.3l-.178.97a5 5 0 0 1-1.265 0l-.177-.97a1.708 1.708 0 0 0-2.255-1.3L4 11.023a5 5 0 0 1-.63-1.084l.754-.638a1.704 1.704 0 0 0 0-2.603l-.755-.637q.248-.581.63-1.084" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="user-settings" viewBox="0 0 32 32">
|
||||||
|
<path fill="currentColor" d="M25.303 16.86a7.5 7.5 0 0 1 2.749 1.596l-.495 1.725a1.52 1.52 0 0 0 1.095 1.892l1.698.423a7.5 7.5 0 0 1-.04 3.189l-1.536.351a1.52 1.52 0 0 0-1.117 1.927l.467 1.514a7.5 7.5 0 0 1-2.737 1.635L24.15 29.84a1.53 1.53 0 0 0-2.192 0l-1.26 1.3a7.5 7.5 0 0 1-2.75-1.597l.495-1.724a1.52 1.52 0 0 0-1.095-1.892l-1.698-.424a7.5 7.5 0 0 1 .04-3.189l1.536-.35a1.52 1.52 0 0 0 1.117-1.928l-.467-1.513a7.5 7.5 0 0 1 2.737-1.635l1.237 1.272a1.53 1.53 0 0 0 2.192 0zM16 17c.387 0 .757.075 1.097.209a8.98 8.98 0 0 0-2.962 8.342c-.995.28-2.192.449-3.635.449C2.04 26 2 20.205 2 20.15V20a3 3 0 0 1 3-3zm7 5a2 2 0 1 0 0 4a2 2 0 0 0 0-4M10.5 4a5.5 5.5 0 1 1 0 11a5.5 5.5 0 0 1 0-11M23 7a4 4 0 1 1 0 8a4 4 0 0 1 0-8" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
|
||||||
|
<symbol id="user-profile" viewBox="0 0 16 16">
|
||||||
|
<path fill="currentColor" d="M1 4.75C1 3.784 1.784 3 2.75 3h10.5c.966 0 1.75.784 1.75 1.75v6.5A1.75 1.75 0 0 1 13.25 13H2.75A1.75 1.75 0 0 1 1 11.25zM2.75 4a.75.75 0 0 0-.75.75v6.5c0 .414.336.75.75.75h10.5a.75.75 0 0 0 .75-.75v-6.5a.75.75 0 0 0-.75-.75zM9.5 6a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zM5.261 7.714a1.357 1.357 0 1 0 0-2.714a1.357 1.357 0 0 0 0 2.714m-1.403.678A.86.86 0 0 0 3 9.25a1.67 1.67 0 0 0 1.265 1.62l.053.014c.62.155 1.267.155 1.886 0l.054-.013a1.67 1.67 0 0 0 1.265-1.62a.86.86 0 0 0-.858-.859z" />
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="sign-out" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M8.5 11.25a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M11 3.5a.5.5 0 0 0-.576-.494l-7 1.07A.5.5 0 0 0 3 4.57v10.86a.5.5 0 0 0 .424.494l7 1.071a.5.5 0 0 0 .576-.494V10h5.172l-.997.874a.5.5 0 0 0 .658.752l1.996-1.75a.5.5 0 0 0 0-.752l-1.996-1.75a.499.499 0 1 0-.658.752l.997.874H11zm-1 .582V15.92L4 15V4.999zM12.5 16H12v-5h1v4.5a.5.5 0 0 1-.5.5M12 8V4h.5a.5.5 0 0 1 .5.5V8z" />
|
||||||
|
</symbol>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
header
|
|
||||||
</div>
|
|
||||||
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>
|
||||||
@@ -1,38 +1,67 @@
|
|||||||
import {writable} from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import type { UserProfile } from '$lib/types/user'; // 修正导入路径后缀
|
||||||
|
|
||||||
export interface AuthStore {
|
export interface AuthStore {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
tokenHead: string | null;
|
tokenHead: string | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
|
user: UserProfile | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let initialToken: string | null = null;
|
const STORAGE_KEY = 'app_auth_state';
|
||||||
let initialTokenHead: string | null = null;
|
|
||||||
|
|
||||||
|
|
||||||
if (browser) {
|
const emptyAuth: AuthStore = {
|
||||||
initialToken = localStorage.getItem('auth_token');
|
token: null,
|
||||||
initialTokenHead = localStorage.getItem('auth_token_head');
|
tokenHead: null,
|
||||||
}
|
isAuthenticated: false,
|
||||||
|
user: null
|
||||||
const initialAuthStore: AuthStore = {
|
|
||||||
token: initialToken,
|
|
||||||
tokenHead: initialTokenHead,
|
|
||||||
isAuthenticated: initialToken !== null
|
|
||||||
}
|
|
||||||
|
|
||||||
const authStatusStore = writable<AuthStore>({
|
|
||||||
token: initialToken,
|
|
||||||
tokenHead: initialTokenHead,
|
|
||||||
isAuthenticated: initialToken !== null
|
|
||||||
})
|
|
||||||
|
|
||||||
export const authStore = {
|
|
||||||
subscribe: authStatusStore.subscribe,
|
|
||||||
set: authStatusStore.set,
|
|
||||||
update: authStatusStore.update,
|
|
||||||
clear: () => {
|
|
||||||
authStatusStore.set(initialAuthStore);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getInitialState = (): AuthStore => {
|
||||||
|
if (browser) {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Auth state parse error', e);
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emptyAuth;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createAuthStore() {
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable<AuthStore>(getInitialState());
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
update,
|
||||||
|
set: (value: AuthStore) => {
|
||||||
|
if (browser) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
set(value);
|
||||||
|
},
|
||||||
|
login: (data: Omit<AuthStore, 'isAuthenticated'>) => {
|
||||||
|
const newState = { ...data, isAuthenticated: true };
|
||||||
|
if (browser) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
|
||||||
|
}
|
||||||
|
set(newState);
|
||||||
|
},
|
||||||
|
logout: () => {
|
||||||
|
if (browser) {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
set(emptyAuth);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authStore = createAuthStore();
|
||||||
@@ -1,49 +1,30 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
|
||||||
interface SidebarState {
|
interface SidebarState {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isExpanded: boolean;
|
|
||||||
isManualOverride: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始状态:默认可能为 true (显示) 或 false (隐藏),根据你的需求设定
|
||||||
export const sidebarStore = writable<SidebarState>({
|
export const sidebarStore = writable<SidebarState>({
|
||||||
isOpen: false,
|
isOpen: true
|
||||||
isExpanded: false,
|
});
|
||||||
isManualOverride: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换侧边栏打开、隐藏(偏移隐藏)状态
|
* 切换侧边栏显示/隐藏状态
|
||||||
*/
|
*/
|
||||||
export const toggleSidebarOpen = () => {
|
export const toggleSidebar = () => {
|
||||||
sidebarStore.update(state => ({
|
sidebarStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
isOpen: !state.isOpen,
|
isOpen: !state.isOpen,
|
||||||
isManualOverride: true,
|
|
||||||
}));
|
}));
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置手动控制状态
|
* 强制设置侧边栏状态 (例如在移动端点击链接后关闭)
|
||||||
*/
|
*/
|
||||||
export const resetManualOverride = () => {
|
export const setSidebarOpen = (isOpen: boolean) => {
|
||||||
sidebarStore.update(state => ({
|
sidebarStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
isManualOverride: false,
|
isOpen
|
||||||
}));
|
}));
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 切换侧边栏展开状态
|
|
||||||
*/
|
|
||||||
export const toggleSidebar = () => {
|
|
||||||
|
|
||||||
sidebarStore.update(state => ({
|
|
||||||
...state,
|
|
||||||
isExpanded: !state.isExpanded,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { type Writable, writable} from 'svelte/store';
|
|
||||||
import type { UserProfile } from '$lib/types/user.ts';
|
|
||||||
|
|
||||||
export const userStateStore:Writable<UserProfile > = writable<UserProfile>( {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
nickname: '',
|
|
||||||
roles: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const initialUserProfile: UserProfile = {
|
|
||||||
id: '',
|
|
||||||
name: '',
|
|
||||||
nickname: '',
|
|
||||||
roles: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const clearUserProfile = () => {
|
|
||||||
userStore.set(initialUserProfile);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userStore = {
|
|
||||||
// 导出 subscribe 方法供组件订阅
|
|
||||||
subscribe: userStateStore.subscribe,
|
|
||||||
|
|
||||||
// 导出 set 方法
|
|
||||||
set: userStateStore.set,
|
|
||||||
|
|
||||||
// 导出 update 方法
|
|
||||||
update: userStateStore.update,
|
|
||||||
|
|
||||||
// 导出清晰的 'clear' 方法
|
|
||||||
clear: clearUserProfile
|
|
||||||
};
|
|
||||||
@@ -11,5 +11,8 @@ export type IconId =
|
|||||||
"success"|
|
"success"|
|
||||||
"error"|
|
"error"|
|
||||||
"warning"|
|
"warning"|
|
||||||
"info"
|
"info"|
|
||||||
|
"settings"|
|
||||||
|
"user-settings" |
|
||||||
|
"user-profile"
|
||||||
;
|
;
|
||||||
@@ -3,7 +3,7 @@ import type { RouteId } from '$app/types';
|
|||||||
|
|
||||||
export interface NavItem {
|
export interface NavItem {
|
||||||
id: string;
|
id: string;
|
||||||
icon: IconId;
|
icon?: IconId;
|
||||||
label: string;
|
label: string;
|
||||||
href: RouteId;
|
href: RouteId;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
export interface UserProfile{
|
export interface UserProfile{
|
||||||
id: string;
|
id: string;
|
||||||
name : string;
|
username : string;
|
||||||
nickname : string;
|
nickname : string;
|
||||||
roles : string[];
|
roles : string[];
|
||||||
|
avatar? : string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
<svg width="12px" height="12px" class="mt-px text-base-content size-2 fill-current opacity-60 sm:inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path></svg>
|
<svg width="12px" height="12px" class="mt-px text-base-content size-2 fill-current opacity-60 sm:inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048"><path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z"></path></svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="dropdown-content shadow bg-base-200 border border-base-content/10 z-[1] rounded-box w-64 max-h-80 overflow-x-hidden overflow-y-auto flex flex-col ">
|
<ul
|
||||||
|
|
||||||
|
class="dropdown-content shadow bg-base-200 border border-base-content/10 z-[1] rounded-box w-64 max-h-80 overflow-x-hidden overflow-y-auto ">
|
||||||
|
|
||||||
{#each DAISYUI_THEME_OPTIONS as theme (theme.value)}
|
{#each DAISYUI_THEME_OPTIONS as theme (theme.value)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,154 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import AppHeader from '$lib/components/layout/app/AppHeader.svelte';
|
||||||
import { resolve } from '$app/paths';
|
import AppSidebar from '$lib/components/layout/app/AppSidebar.svelte';
|
||||||
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();
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-screen bg-base-300 overflow-hidden relative">
|
<div class="flex h-screen bg-base-300 overflow-hidden relative">
|
||||||
|
|
||||||
{#if $sidebarStore.isOpen}
|
<AppSidebar />
|
||||||
<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
|
<div class="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||||
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 pt-6 menu-vertical transition-all duration-1000 w-full">
|
<AppHeader />
|
||||||
{#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">
|
<main class="flex-1 overflow-y-auto p-4">
|
||||||
<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()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
0
src/routes/app/settings/users/+page.svelte
Normal file
0
src/routes/app/settings/users/+page.svelte
Normal file
@@ -37,8 +37,12 @@
|
|||||||
}
|
}
|
||||||
} catch (e:unknown) {
|
} catch (e:unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
|
if (e.name === 'TypeError' && e.message.includes('fetch')) {
|
||||||
|
toast.error("网络错误,请检查网络连接");
|
||||||
|
} else {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error(e.message);
|
toast.error(e.message || '登录失败,请重试');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error('登录失败,请重试');
|
toast.error('登录失败,请重试');
|
||||||
|
|||||||
Reference in New Issue
Block a user