diff --git a/src/lib/api/httpClient.ts b/src/lib/api/httpClient.ts index 92ea48e..58b7aa3 100644 --- a/src/lib/api/httpClient.ts +++ b/src/lib/api/httpClient.ts @@ -61,66 +61,68 @@ export class HttpError extends Error { } -const httpRequest= async ( - url:string, +const httpRequest = async ( + url: string, method: HttpMethod, options: RequestOptions = {} -):Promise> =>{ +): Promise> => { const fullUrl = `${API_BASE_URL}${url}`; - const { body, headers, ...rest} = options; + const { body, headers, ...rest } = options; - const requestHeaders: Record = normalizeHeaders(headers); - let requestBody:BodyInit | undefined; + const requestHeaders: Record = normalizeHeaders(headers); + let requestBody: BodyInit | undefined; - if (body instanceof FormData){ - requestBody = body; - }else if (body){ - requestHeaders['content-type'] = 'application/json'; - requestBody = JSON.stringify(body); + const canHaveBody = method !== 'GET' ; + + // 【修改点 2】:只有在允许携带 Body 时才处理 + if (canHaveBody) { + if (body instanceof FormData) { + requestBody = body; + } else if (body) { + requestHeaders['content-type'] = 'application/json'; + requestBody = JSON.stringify(body); + } } + // ... Token 处理逻辑保持不变 ... if (currentToken && currentTokenHead) { requestHeaders['authorization'] = `${currentTokenHead} ${currentToken}`; } try { - const response = await fetch(fullUrl,{ + const response = await fetch(fullUrl, { method, headers: requestHeaders, - body: requestBody, + // 【修改点 3】:确保 GET 请求的 body 显式为 undefined + // 虽然通常 undefined 是被允许的,但加上 canHaveBody 判断更加严谨 + body: canHaveBody ? requestBody : undefined, ...rest - }) + }); if (!response.ok) { - let errorDetail; - try { - - errorDetail = await response.json() - - }catch (e){ + errorDetail = await response.json(); + } catch (e) { console.error('Error parsing JSON:', e); - errorDetail = await response.text() + errorDetail = await response.text(); } - const message = `HTTP Error ${response.status} (${response.statusText})`; throw new HttpError(message, response.status, errorDetail); } const contentType = response.headers.get('Content-Type'); - if (contentType && contentType.includes('application/json')){ - return (await response.json() ) as ApiResult; + if (contentType && contentType.includes('application/json')) { + return (await response.json()) as ApiResult; } - return {code:200, msg:'OK', data:null} ; + return { code: 200, msg: 'OK', data: null } ; // 这里的 as any 是为了兼容 T 可能是 null 的情况 - }catch (error){ + } catch (error) { console.error(`API Request Failed to ${fullUrl}:`, error); throw error; } - -} +}; export const api = { diff --git a/src/lib/api/services/authService.ts b/src/lib/api/services/authService.ts index 7088d23..f11c76e 100644 --- a/src/lib/api/services/authService.ts +++ b/src/lib/api/services/authService.ts @@ -3,7 +3,6 @@ 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 = { /** @@ -19,15 +18,11 @@ export const authService = { 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({tokenHead,token}); // 4. 最终确认登录状态(更新完整信息并持久化) // 这里调用 Store 封装好的 login 方法,它会负责写入 localStorage @@ -40,9 +35,7 @@ export const authService = { return response.data; } catch (error) { - // 5. 安全回滚 - // 如果获取用户信息失败(比如 Token 虽然返回了但无效,或者网络波动), - // 我们应该立即清除刚才设置的临时 Token,防止应用处于中间状态。 + console.error('获取用户信息失败,回滚登录状态', error); authStore.logout(); throw error; // 继续抛出错误给 UI 层处理 @@ -53,10 +46,7 @@ export const authService = { * 登出流程 */ logout: async () => { - // 逻辑大大简化:只负责调用 Store 和 UI 反馈 authStore.logout(); toast.success('退出登录成功'); - - // 如果需要调用后端登出接口(使 Token 失效),在这里 await api.post('/auth/logout') } }; \ No newline at end of file diff --git a/src/lib/api/services/userService.ts b/src/lib/api/services/userService.ts index dbc34af..7a64817 100644 --- a/src/lib/api/services/userService.ts +++ b/src/lib/api/services/userService.ts @@ -1,12 +1,23 @@ import { api } from '$lib/api/httpClient.ts'; import type { UserProfile } from '$lib/types/user.ts'; +import type { PageResult } from '$lib/types/dataTable.ts'; export const userService = { - getUserProfile: async () => { + getUserProfile: async ({ tokenHead, token}) => { const response = await api.get('/user/profile'); if (response.code != 200 || !response.data){ throw new Error(response.msg); } return response.data; - } + }, + getAllUsers: async ({ page, size}: { page: number, size: number}) => { + const formData = new FormData(); + formData.append('pageNum', page.toString()); + formData.append('pageSize', size.toString()); + const response = await api.get[]>('/user/all', {body: formData}); + if (response.code != 200 || !response.data){ + throw new Error(response.msg); + } + return response.data; + }, } \ No newline at end of file diff --git a/src/lib/components/DataTable.svelte b/src/lib/components/DataTable.svelte new file mode 100644 index 0000000..a45ee6e --- /dev/null +++ b/src/lib/components/DataTable.svelte @@ -0,0 +1,148 @@ + + +
+
+
+ {#if selectedIds.size > 0} +
已选 {selectedIds.size} 项
+ + {:else} + + {/if} +
+
+
+ +
+ + + + + {#each columns as col(col.key)} + + {/each} + + + + + + {#if loading} + {#each Array(5) as _} + + {/each} + {:else if data.records.length === 0} + + + + {:else} + {#each data.records as row (row.id)} + + + + {#each columns as col} + + {/each} + + + + {/each} + {/if} + +
+ + + {col.label} + 操作
+ 暂无数据 +
+ + + + {String(row[col.key] ?? '-')} + + +
+ + +
+
+
+ + {#if data.total > 0} +
+ 第 {data.current} / {data.pages} 页 +
+ + + +
+
+ {/if} +
\ No newline at end of file diff --git a/src/lib/components/icon/Sprite.svelte b/src/lib/components/icon/Sprite.svelte index bf17851..9820440 100644 --- a/src/lib/components/icon/Sprite.svelte +++ b/src/lib/components/icon/Sprite.svelte @@ -44,12 +44,12 @@ - - + + - - + + @@ -107,10 +107,10 @@ - - - + + + @@ -123,4 +123,6 @@ + + \ No newline at end of file diff --git a/src/lib/components/layout/app/AppSidebar.svelte b/src/lib/components/layout/app/AppSidebar.svelte index 0772217..a53ff79 100644 --- a/src/lib/components/layout/app/AppSidebar.svelte +++ b/src/lib/components/layout/app/AppSidebar.svelte @@ -5,24 +5,24 @@ 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'; + import { authStore } from '$lib/stores/authStore'; + import type { NavItem, ProcessedNavItem } from '$lib/types/layout'; + import { authService } from '$lib/api/services/authService'; - // 模拟一些数据,增加了分组的概念(如果需要) + + // 1. 模拟数据:包含三层结构 const rawNavItems: NavItem[] = [ { id: 'dashboard', label: '仪表盘', icon: 'home', - href: '/app/dashboard', + href: '/app/dashboard' }, { id: 'statistics', label: '数据看板', - icon: 'data-pie', - href: '/app/statistics', + icon: 'data', + href: '/app/statistics' }, { id: 'settings', @@ -31,50 +31,78 @@ href: '/app/settings', subItems: [ { - id: 'users', - label: '用户管理', - href: '/app/settings/users', + id: 'auth', + label: '认证管理', + href: '/app/settings/auth', + icon: 'auth', + subItems: [ + { + id: 'users', + label: '用户管理', + href: '/app/settings/auth/users' + }, + { + id: 'roles', + label: '角色权限', + href: '/app/settings/auth/roles' + }, + { + id: 'permissions', + label: '权限管理', + href: '/app/settings/auth/permissions' + } + ] }, + { - id: 'roles', - label: '角色权限', - href: '/app/settings/roles', - }, + id: 'advanced', + label: '高级设置', + href: '/app/settings/advanced', + subItems: [ + { + id: 'logs', + label: '安全日志', + href: '/app/settings/advanced/logs' + }, + { + id: 'backup', + label: '备份恢复', + href: '/app/settings/advanced/backup' + } + ] + } ] } ]; /** - * 递归计算高亮状态 - * 只要子元素有一个是激活的,父元素就算激活 + * 递归计算高亮状态 (强类型版本) */ - function processNavItems(items: NavItem[], currentPath: string): any[] { - return items.map(item => { - // 判断当前项是否匹配 - const isSelfActive = item.href === '/' - ? currentPath === '/' - : currentPath.startsWith(item.href); + function processNavItems(items: NavItem[], currentPath: string): ProcessedNavItem[] { + return items.map((item) => { + const isSelfActive = + item.href === '/' ? currentPath === '/' : currentPath.startsWith(item.href); - // 处理子项 - let processedSubItems = undefined; + let processedSubItems: ProcessedNavItem[] | undefined = undefined; let isChildActive = false; if (item.subItems) { + // 递归调用 processedSubItems = processNavItems(item.subItems, currentPath); - // 检查是否有子项被激活 - isChildActive = processedSubItems.some(sub => sub.isActive || sub.isChildActive); + // 检查子项激活状态 + isChildActive = processedSubItems.some((sub) => sub.isActive || sub.isChildActive); } return { ...item, - subItems: processedSubItems, - isActive: isSelfActive, // 自身路径匹配 - isChildActive: isChildActive // 子路径匹配(用于展开菜单) + subItems: processedSubItems, // 这里类型现在是 ProcessedNavItem[] + isActive: isSelfActive, + isChildActive: isChildActive }; }); } - // 使用 $derived 动态计算 + // 使用 $derived 动态计算,类型自动推断为 ProcessedNavItem[] let navItems = $derived(processNavItems(rawNavItems, page.url.pathname)); function handleMobileClose() { @@ -85,85 +113,87 @@ } + +{#snippet menuItem(item: ProcessedNavItem)} +
  • + {#if item.subItems && item.subItems.length > 0} +
    + + {#if item.icon} + + {/if} + {item.label} + +
      + {#each item.subItems as subItem (subItem.id)} + + {@render menuItem(subItem)} + {/each} +
    +
    + {:else} + + {#if item.icon} + + {:else} + + + {/if} + {item.label} + + {/if} +
  • +{/snippet} + {#if $sidebarStore.isOpen}
    e.key === 'Escape' && handleMobileClose()} transition:fade={{ duration: 200 }} >
    {/if}