diff --git a/src/lib/api/services/authService.ts b/src/lib/api/services/authService.ts index 78c9609..7088d23 100644 --- a/src/lib/api/services/authService.ts +++ b/src/lib/api/services/authService.ts @@ -1,48 +1,62 @@ - -import {api} from '$lib/api/httpClient.ts' -import type { AuthResponse, LoginPayload } from '$lib/types/auth.ts'; -import { browser } from '$app/environment'; -import { authStore } from '$lib/stores/authStore.ts'; -import { userService } from '$lib/api/services/userService.ts'; -import { userStore } from '$lib/stores/userStore.ts'; +import { api } from '$lib/api/httpClient'; // 通常不需要 .ts 后缀 +import type { AuthResponse, LoginPayload } from '$lib/types/auth'; +import { authStore } from '$lib/stores/authStore'; +import { userService } from '$lib/api/services/userService'; +import { toast } from '$lib/stores/toastStore'; +import { get } from 'svelte/store'; export const authService = { + /** + * 登录流程 + */ login: async (payload: LoginPayload): Promise => { + // 1. 调用登录接口 const response = await api.post('/auth/login', payload); - if (response.code != 200 || !response.data){ - throw new Error(response.msg); - } - if (browser){ - authService._setToken(response.data.token, response.data.tokenHead) + if (response.code !== 200 || !response.data) { + throw new Error(response.msg || '登录失败'); } - const userProfile = await userService.getUserProfile(); + const { token, tokenHead } = response.data; - if (browser){ - userStore.set(userProfile) + // 2. 临时设置 Token 到 Store + // 这一步是必须的,因为接下来的 userService.getUserProfile() + // 里的 API 请求拦截器需要读取 Store 中的 Token 才能通过鉴权。 + // 我们先以“部分登录”的状态更新 Store。 + authStore.update(s => ({ ...s, token, tokenHead, isAuthenticated: true })); + + try { + // 3. 获取用户信息 + const userProfile = await userService.getUserProfile(); + + // 4. 最终确认登录状态(更新完整信息并持久化) + // 这里调用 Store 封装好的 login 方法,它会负责写入 localStorage + authStore.login({ + token, + tokenHead, + user: userProfile + }); + + return response.data; + + } catch (error) { + // 5. 安全回滚 + // 如果获取用户信息失败(比如 Token 虽然返回了但无效,或者网络波动), + // 我们应该立即清除刚才设置的临时 Token,防止应用处于中间状态。 + console.error('获取用户信息失败,回滚登录状态', error); + authStore.logout(); + throw error; // 继续抛出错误给 UI 层处理 } - - - return response.data; }, + + /** + * 登出流程 + */ logout: async () => { + // 逻辑大大简化:只负责调用 Store 和 UI 反馈 + authStore.logout(); + toast.success('退出登录成功'); - if (browser){ - - 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); + // 如果需要调用后端登出接口(使 Token 失效),在这里 await api.post('/auth/logout') } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/lib/components/icon/Sprite.svelte b/src/lib/components/icon/Sprite.svelte index ba744f7..bf17851 100644 --- a/src/lib/components/icon/Sprite.svelte +++ b/src/lib/components/icon/Sprite.svelte @@ -44,43 +44,12 @@ - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - + + @@ -137,4 +106,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/lib/components/layout/Header.svelte b/src/lib/components/layout/Header.svelte deleted file mode 100644 index cb4290e..0000000 --- a/src/lib/components/layout/Header.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -
- header -
\ No newline at end of file diff --git a/src/lib/components/layout/app/AppHeader.svelte b/src/lib/components/layout/app/AppHeader.svelte new file mode 100644 index 0000000..9502158 --- /dev/null +++ b/src/lib/components/layout/app/AppHeader.svelte @@ -0,0 +1,60 @@ + + +
+
+ +
+ +
+ + + {#if $authStore.user } + + + {:else} +
+
+ +
+
+ {/if} +
+
\ No newline at end of file diff --git a/src/lib/components/layout/app/AppSidebar.svelte b/src/lib/components/layout/app/AppSidebar.svelte new file mode 100644 index 0000000..0772217 --- /dev/null +++ b/src/lib/components/layout/app/AppSidebar.svelte @@ -0,0 +1,213 @@ + + +{#if $sidebarStore.isOpen} +
e.key === 'Escape' && handleMobileClose()} + transition:fade={{ duration: 200 }} + >
+ + +{/if} + + \ No newline at end of file diff --git a/src/lib/stores/authStore.ts b/src/lib/stores/authStore.ts index fc80f2c..7b97fa7 100644 --- a/src/lib/stores/authStore.ts +++ b/src/lib/stores/authStore.ts @@ -1,38 +1,67 @@ -import {writable} from 'svelte/store'; +import { writable } from 'svelte/store'; import { browser } from '$app/environment'; +import type { UserProfile } from '$lib/types/user'; // 修正导入路径后缀 export interface AuthStore { token: string | null; tokenHead: string | null; isAuthenticated: boolean; + user: UserProfile | null; } -let initialToken: string | null = null; -let initialTokenHead: string | null = null; +const STORAGE_KEY = 'app_auth_state'; -if (browser) { - initialToken = localStorage.getItem('auth_token'); - initialTokenHead = localStorage.getItem('auth_token_head'); +const emptyAuth: AuthStore = { + token: null, + tokenHead: null, + isAuthenticated: false, + user: null +}; + + +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(getInitialState()); + + return { + subscribe, + update, + set: (value: AuthStore) => { + if (browser) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(value)); + } + set(value); + }, + login: (data: Omit) => { + 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); + } + }; } -const initialAuthStore: AuthStore = { - token: initialToken, - tokenHead: initialTokenHead, - isAuthenticated: initialToken !== null -} - -const authStatusStore = writable({ - token: initialToken, - tokenHead: initialTokenHead, - isAuthenticated: initialToken !== null -}) - -export const authStore = { - subscribe: authStatusStore.subscribe, - set: authStatusStore.set, - update: authStatusStore.update, - clear: () => { - authStatusStore.set(initialAuthStore); - }, -}; \ No newline at end of file +export const authStore = createAuthStore(); \ No newline at end of file diff --git a/src/lib/stores/sidebarStore.ts b/src/lib/stores/sidebarStore.ts index 50dd5d9..dbe0042 100644 --- a/src/lib/stores/sidebarStore.ts +++ b/src/lib/stores/sidebarStore.ts @@ -1,49 +1,30 @@ import { writable } from 'svelte/store'; - interface SidebarState { isOpen: boolean; - isExpanded: boolean; - isManualOverride: boolean; } - +// 初始状态:默认可能为 true (显示) 或 false (隐藏),根据你的需求设定 export const sidebarStore = writable({ - isOpen: false, - isExpanded: false, - isManualOverride: false, -}) - - + isOpen: true +}); /** - * 切换侧边栏打开、隐藏(偏移隐藏)状态 + * 切换侧边栏显示/隐藏状态 */ -export const toggleSidebarOpen = () => { +export const toggleSidebar = () => { sidebarStore.update(state => ({ ...state, isOpen: !state.isOpen, - isManualOverride: true, })); -} +}; /** - * 重置手动控制状态 + * 强制设置侧边栏状态 (例如在移动端点击链接后关闭) */ -export const resetManualOverride = () => { +export const setSidebarOpen = (isOpen: boolean) => { sidebarStore.update(state => ({ ...state, - isManualOverride: false, + isOpen })); -} - -/** - * 切换侧边栏展开状态 - */ -export const toggleSidebar = () => { - - sidebarStore.update(state => ({ - ...state, - isExpanded: !state.isExpanded, - })); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/lib/stores/userStore.ts b/src/lib/stores/userStore.ts deleted file mode 100644 index 7e7452d..0000000 --- a/src/lib/stores/userStore.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { type Writable, writable} from 'svelte/store'; -import type { UserProfile } from '$lib/types/user.ts'; - -export const userStateStore:Writable = writable( { - 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 -}; \ No newline at end of file diff --git a/src/lib/types/icon-ids.ts b/src/lib/types/icon-ids.ts index 7ac85d4..5df3cfc 100644 --- a/src/lib/types/icon-ids.ts +++ b/src/lib/types/icon-ids.ts @@ -11,5 +11,8 @@ export type IconId = "success"| "error"| "warning"| - "info" + "info"| + "settings"| + "user-settings" | + "user-profile" ; \ No newline at end of file diff --git a/src/lib/types/layout.ts b/src/lib/types/layout.ts index 7a672a0..c221cb2 100644 --- a/src/lib/types/layout.ts +++ b/src/lib/types/layout.ts @@ -3,7 +3,7 @@ import type { RouteId } from '$app/types'; export interface NavItem { id: string; - icon: IconId; + icon?: IconId; label: string; href: RouteId; isActive?: boolean; diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts index 7d247a6..52b8cf0 100644 --- a/src/lib/types/user.ts +++ b/src/lib/types/user.ts @@ -1,8 +1,9 @@ export interface UserProfile{ id: string; - name : string; + username : string; nickname : string; roles : string[]; + avatar? : string; } diff --git a/src/lib/widget/ThemeSelector.svelte b/src/lib/widget/ThemeSelector.svelte index 808c9b3..58f21dd 100644 --- a/src/lib/widget/ThemeSelector.svelte +++ b/src/lib/widget/ThemeSelector.svelte @@ -14,7 +14,9 @@ -