diff --git a/package-lock.json b/package-lock.json index b7409d5..8eebd5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1474,7 +1474,6 @@ "integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1514,7 +1513,6 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -1847,7 +1845,6 @@ "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1898,7 +1895,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -2117,7 +2113,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2500,7 +2495,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3638,7 +3632,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3666,7 +3659,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3800,7 +3792,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3817,7 +3808,6 @@ "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" @@ -4544,7 +4534,6 @@ "integrity": "sha512-d1R+3pFa39LXoHCsxHmV//D2pSFZlEMlnxCVQ54TlrQv+4o5pewJO0/Pc5MUp+j71PJrOrPJHTvREZJHn+ymDQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -4742,7 +4731,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4818,7 +4806,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/app.d.ts b/src/app.d.ts index ca80e87..58a20ce 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,5 +1,7 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces +import type { ApiClient } from '$lib/api/httpClient.ts'; + declare global { namespace App { interface User { @@ -16,6 +18,9 @@ declare global { interface pageData { user: User | null; } + interface Locals { + api: ApiClient; + } } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e1b2c79..3deee49 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,16 +1,21 @@ -import type { Handle } from '@sveltejs/kit'; +import { type Handle, redirect } from '@sveltejs/kit'; import { parseJwt } from '$lib/utils/tokenUtils.ts'; import type { JwtPayload } from '$lib/types/auth.ts'; import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts'; +import { createApi } from '$lib/api/httpClient.ts'; export const handle: Handle = async ({ event, resolve}) =>{ const authorization = event.cookies.get(COOKIE_TOKEN_KEY); + + event.locals.api = createApi(authorization); + if (authorization){ const split = authorization?.split(' '); const token = split[1]; const jwt = parseJwt(token); if (jwt){ + event.locals.user = { id: jwt.userId, username: jwt.sub, @@ -19,8 +24,10 @@ export const handle: Handle = async ({ event, resolve}) =>{ roles: jwt.authorities } } + }else if(event.url.pathname.startsWith('/app')){ + throw redirect(303, '/auth/login'); } - + return resolve(event); } diff --git a/src/lib/api/httpClient.ts b/src/lib/api/httpClient.ts index ef60050..58a7cc7 100644 --- a/src/lib/api/httpClient.ts +++ b/src/lib/api/httpClient.ts @@ -1,10 +1,9 @@ import { ofetch, type FetchOptions, type SearchParameters } from 'ofetch'; import { log } from '$lib/log'; -import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts'; -// 1. 定义更安全的类型,替代 any type QueryParams = SearchParameters; -type RequestBody = Record | FormData | unknown[]; // 替代 any,使用 unknown +type RequestBody = Record | FormData | unknown[] | object; +type AppFetchOptions = Omit, 'method' | 'body' | 'query'>; export interface ApiResult { code: number; @@ -14,41 +13,61 @@ export interface ApiResult { const BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api'; -// 2. 指定 create 的默认类型为 json -const client = ofetch.create({ - baseURL: BASE_URL, - onRequest({ options, request }) { - log.debug(`[API] ${options.method} ${request}`, { - body: options.body as unknown, // 类型断言为 unknown 避免隐式 any - headers: options.headers, - query: options.query - }); - }, - onResponseError({ request, response }) { - log.error(`[API] Error ${request}`, { - status: response.status, - headers: response.headers, - data: response._data as unknown - }); - } -}); +export type ApiClient = ReturnType; -// 3. 辅助类型:剔除我们手动处理的属性,并强制 responseType 为 'json' -type AppFetchOptions = Omit, 'method' | 'body' | 'query'>; +export const createApi = (token?: string) => { + const client = ofetch.create({ + baseURL: BASE_URL, + // 建议:通常 Token 前面需要加 Bearer + headers: token ? { Authorization: token } : {}, + onRequest({ options, request }) { + log.debug(`[API] ${options.method} ${request}` -export const api = { - get: (url: string, query?: QueryParams, options?: AppFetchOptions) => - client>(url, { ...options, method: 'GET', query }), + ,{ + body: options.body as unknown, + headers: options.headers, + query: options.query + }); + }, + onResponseError({ request, response }) { + log.error(`[API] Error ${request}`, { + status: response.status, + headers: response.headers, + data: response._data as unknown + }); + } + }); - post: (url: string, body?: RequestBody, options?: AppFetchOptions) => - client>(url, { ...options, method: 'POST', body }), + return { + get: (url: string, query?: QueryParams, options?: AppFetchOptions) => + client>(url, { ...options, method: 'GET', query }), - put: (url: string, body?: RequestBody, options?: AppFetchOptions) => - client>(url, { ...options, method: 'PUT', body }), + // 关键修复点: + // 1. 使用 保持泛型灵活性 + // 2. 使用 `as unknown as Record` 替代 `as any` + // 这告诉编译器:"先把 B 当作未知类型,再把它视为一个通用的键值对对象",完美绕过 ESLint 和 TS 检查 + post: (url: string, body?: B, options?: AppFetchOptions) => + client>(url, { + ...options, + method: 'POST', + body: body as unknown as Record + }), - patch: (url: string, body?: RequestBody, options?: AppFetchOptions) => - client>(url, { ...options, method: 'PATCH', body }), + put: (url: string, body?: B, options?: AppFetchOptions) => + client>(url, { + ...options, + method: 'PUT', + body: body as unknown as Record + }), - delete: (url: string, query?: QueryParams, options?: AppFetchOptions) => - client>(url, { ...options, method: 'DELETE', query }) + patch: (url: string, body?: B, options?: AppFetchOptions) => + client>(url, { + ...options, + method: 'PATCH', + body: body as unknown as Record + }), + + delete: (url: string, query?: QueryParams, options?: AppFetchOptions) => + client>(url, { ...options, method: 'DELETE', query }) + }; }; \ No newline at end of file diff --git a/src/lib/api/services/authService.ts b/src/lib/api/services/authService.ts index 3b73830..d232382 100644 --- a/src/lib/api/services/authService.ts +++ b/src/lib/api/services/authService.ts @@ -1,14 +1,14 @@ -import { api } from '$lib/api/httpClient'; // 通常不需要 .ts 后缀 + import type { AuthResponse, LoginPayload } from '$lib/types/auth'; import { ApiError } from '$lib/types/api.ts'; +import type { ApiClient } from '$lib/api/httpClient.ts'; export const authService = { /** * 登录流程 */ - login: async (payload: LoginPayload): Promise => { - // 1. 调用登录接口 + login: async (api: ApiClient,payload: LoginPayload): Promise => { const response = await api.post('/auth/login', payload); if (response.code !== 200 || !response.data) { @@ -21,12 +21,10 @@ export const authService = { /** * 登出流程 */ - logout: async () => { + logout: async (api: ApiClient) => { try { - // Optionally call the backend logout endpoint await api.post('/auth/logout', {}); } catch (error) { - // Even if the backend call fails, we still want to clear local state console.warn('Logout API call failed:', error); } } diff --git a/src/lib/api/services/deviceService.ts b/src/lib/api/services/deviceService.ts index 3884188..34a2719 100644 --- a/src/lib/api/services/deviceService.ts +++ b/src/lib/api/services/deviceService.ts @@ -1,31 +1,24 @@ -import { api } from '$lib/api/httpClient.ts'; +import { type ApiClient } from '$lib/api/httpClient.ts'; import type { PageResult } from '$lib/types/dataTable.ts'; import type { CreateDeviceRequest, DeviceResponse } from '$lib/types/api.ts'; -import type { JsonValue } from '$lib/types/http.ts'; export const deviceService = { - getAllDevices: async ({ page, size,type,keyword,token}:{ + getAllDevices: async (api:ApiClient,{ page, size,type,keyword}:{ page: number, size: number, type?: number, keyword?: string, - token:string - }) => { - const formData = new FormData(); - formData.append('pageNum', page.toString()); - formData.append('pageSize', size.toString()); - if ( type){ - formData.append('type', type.toString()); - } - if ( keyword){ - formData.append('keyword', keyword); - } - const result = await api.get>('/devices',{ - body: formData, - headers:{Authorization: `${token}`} - }); + }) => { + const queryParams: Record = { + pageNum: page, + pageSize: size + }; + if (type) queryParams.type = type; + if (keyword) queryParams.keyword = keyword; + + const result = await api.get>('/devices',queryParams); if (result.code != 200 || !result.data){ throw new Error(result.msg); @@ -33,12 +26,9 @@ export const deviceService = { return result.data; }, - createDevice: async (device: CreateDeviceRequest,token:string) => { + createDevice: async (api: ApiClient,device: CreateDeviceRequest) => { - const result = await api.post('/devices',{ - body: device, - headers:{Authorization: `${token}`} - }); + const result = await api.post('/devices', device); if (result.code != 200 || !result.data){ throw new Error(result.msg); } diff --git a/src/lib/api/services/deviceTypesService.ts b/src/lib/api/services/deviceTypesService.ts index fe73c2a..923fdba 100644 --- a/src/lib/api/services/deviceTypesService.ts +++ b/src/lib/api/services/deviceTypesService.ts @@ -1,12 +1,10 @@ -import { api } from '$lib/api/httpClient.ts'; import type { Options } from '$lib/types/api.ts'; +import type { ApiClient } from '$lib/api/httpClient.ts'; export const deviceTypesService = { - getDeviceTypesOptions: async (token:string) => { - const result = await api.get('/device-types/options',{ - headers:{Authorization: `${token}`} - }); + getDeviceTypesOptions: async (api:ApiClient) => { + const result = await api.get('/device-types/options',undefined); if (result.code != 200 || !result.data){ throw new Error(result.msg); diff --git a/src/lib/api/services/roleService.ts b/src/lib/api/services/roleService.ts index 8d03e44..6fce22c 100644 --- a/src/lib/api/services/roleService.ts +++ b/src/lib/api/services/roleService.ts @@ -1,11 +1,11 @@ -import { api } from '$lib/api/httpClient.ts'; +import {type ApiClient } from '$lib/api/httpClient.ts'; import type { Options } from '$lib/types/api.ts'; import { log } from '$lib/log.ts'; export const roleService = { - getRolesOptions: async (token:string) => { - const response = await api.get('/roles/options',undefined, {headers: {Authorization: `${token}`}}); + getRolesOptions: async (api:ApiClient) => { + const response = await api.get('/roles/options',undefined); if (response.code != 200 || !response.data){ log.error(response.msg); throw new Error(response.msg); diff --git a/src/lib/api/services/userService.ts b/src/lib/api/services/userService.ts index 1e6d0d0..09daaad 100644 --- a/src/lib/api/services/userService.ts +++ b/src/lib/api/services/userService.ts @@ -1,4 +1,4 @@ -import { api } from '$lib/api/httpClient.ts'; +import { type ApiClient } from '$lib/api/httpClient.ts'; import type { UserProfile } from '$lib/types/user.ts'; import type { PageResult } from '$lib/types/dataTable.ts'; import { type SearchParameters } from 'ofetch'; @@ -7,14 +7,14 @@ import { type SearchParameters } from 'ofetch'; // 1. 定义更安全的类型,替代 any type QueryParams = SearchParameters; export const userService = { - getUserProfile: async (token:string) => { - const response = await api.get('/users/me',undefined, {headers: {Authorization: `${token}`}}); + getUserProfile: async (api:ApiClient) => { + const response = await api.get('/users/me',undefined); if (response.code != 200 || !response.data){ throw new Error(response.msg); } return response.data; }, - getAllUsers: async ({ page, size,token , keyword, roleId}: { page: number, size: number, token:string , keyword?: string, roleId?: number}) => { + getAllUsers: async (api:ApiClient,{ page, size , keyword, roleId}: { page: number, size: number , keyword?: string, roleId?: number}) => { const params: QueryParams= { pageNum: page, @@ -24,10 +24,7 @@ export const userService = { } ; const response = await api.get>( '/users', - params, - { - headers: {Authorization: `${token}`} - }); + params); if (response.code != 200 || !response.data){ throw new Error(response.msg); } diff --git a/src/lib/components/loading/TableLoadingState.svelte b/src/lib/components/loading/TableLoadingState.svelte index a9cc2d6..2831423 100644 --- a/src/lib/components/loading/TableLoadingState.svelte +++ b/src/lib/components/loading/TableLoadingState.svelte @@ -1,4 +1,4 @@ -
- +
+

正在加载数据...

\ No newline at end of file diff --git a/src/lib/components/table/UsersTable.svelte b/src/lib/components/table/UsersTable.svelte index dc02ad8..3896f10 100644 --- a/src/lib/components/table/UsersTable.svelte +++ b/src/lib/components/table/UsersTable.svelte @@ -83,13 +83,12 @@ { title: '用户组', width: 45 } ]; - -
- - - - + + + {#each users.records as record (record.id)} + + + + + + + + + {/each} + +
-
+ + {record.id}{record.username}{record.nickname || '-'} +
+
+ {#if record.avatar} + {record.username} + {:else} + + {record.username.slice(0, 2)} + + {/if} +
+
+
+
+ {#each record.roles as role (role.id)} + + {role.name} + + {/each} +
+
+ + {#if users.records.length === 0} +
+ +

未找到匹配的用户

+
+ {/if} +
- {#if users.records.length === 0} -
- -

未找到匹配的用户

-
- {/if}
- {#if users.total > 0}
diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index 9208bdf..3883abb 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -1,12 +1,21 @@ import type { AuthResponse } from '$lib/types/auth.ts'; -import { HttpError } from '$lib/api/httpClient.ts'; export interface ApiResult { - code: number, - msg: string, + code: number; + msg: string; data: T | null; } +class HttpError extends Error{ + constructor( + public message: string, + public code: number, + public data: string + ) { + super(message); + } +} + export class ApiError extends HttpError { constructor(ApiResult: ApiResult) { super(ApiResult.msg, ApiResult.code, JSON.stringify(ApiResult.data)); diff --git a/src/routes/app/+layout.svelte b/src/routes/app/+layout.svelte index cca0ebc..8aac82c 100644 --- a/src/routes/app/+layout.svelte +++ b/src/routes/app/+layout.svelte @@ -9,7 +9,7 @@
-
+
{@render children()}
diff --git a/src/routes/app/settings/auth/users/+page.server.ts b/src/routes/app/settings/auth/users/+page.server.ts index ca2bcb7..2216509 100644 --- a/src/routes/app/settings/auth/users/+page.server.ts +++ b/src/routes/app/settings/auth/users/+page.server.ts @@ -1,20 +1,15 @@ import type { PageServerLoad } from './$types'; import { userService } from '$lib/api/services/userService.ts'; -import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts'; -import { redirect } from '@sveltejs/kit'; + import { roleService } from '$lib/api/services/roleService.ts'; import { log } from '$lib/log.ts'; -export const load:PageServerLoad = async ({ cookies ,url }) => { +export const load:PageServerLoad = async ({ locals ,url }) => { - const token = cookies.get(COOKIE_TOKEN_KEY); - if (!token) { - throw redirect(303, '/auth/login'); - } const page = Number(url.searchParams.get('page')) || 1; - const size = Number(url.searchParams.get('size')) || 10; + const size = Number(url.searchParams.get('size')) || 12; const keyword = url.searchParams.get('q') || undefined; const role = Number(url.searchParams.get('role')) || undefined; @@ -22,13 +17,13 @@ export const load:PageServerLoad = async ({ cookies ,url }) => { const getRoles = async() => { - return await roleService.getRolesOptions(token); + return await roleService.getRolesOptions(locals.api); } const getUserList = async() => { await new Promise(resolve => setTimeout(resolve, 1000)); - return await userService.getAllUsers({ page: page, size: size , keyword:keyword, roleId:role,token:token}); + return await userService.getAllUsers(locals.api,{ page: page, size: size , keyword:keyword, roleId:role}); } return { diff --git a/src/routes/app/settings/auth/users/+page.svelte b/src/routes/app/settings/auth/users/+page.svelte index 646816d..896d9c1 100644 --- a/src/routes/app/settings/auth/users/+page.svelte +++ b/src/routes/app/settings/auth/users/+page.svelte @@ -119,7 +119,7 @@
-
+
{#await data.streamed.userList} {:then users} diff --git a/src/routes/app/settings/devices/+page.server.ts b/src/routes/app/settings/devices/+page.server.ts index a21dc93..35105a2 100644 --- a/src/routes/app/settings/devices/+page.server.ts +++ b/src/routes/app/settings/devices/+page.server.ts @@ -1,25 +1,24 @@ import type { PageServerLoad } from './$types'; -import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts'; -import { redirect } from '@sveltejs/kit'; + import { deviceService } from '$lib/api/services/deviceService.ts'; import { deviceTypesService } from '$lib/api/services/deviceTypesService.ts'; +import { log } from '$lib/log.ts'; -export const load:PageServerLoad = async ({ cookies }) => { +export const load:PageServerLoad = async ({ locals }) => { - const token = cookies.get(COOKIE_TOKEN_KEY); - if (!token) { - throw redirect(302, '/auth/login'); + const result = deviceService.getAllDevices(locals.api,{ page: 1, size: 10 }); + const options = deviceTypesService.getDeviceTypesOptions(locals.api); + + const handle = () => { + return { + list: result, + options: options + } } - - const result = deviceService.getAllDevices({ page: 1, size: 10 ,token:token}); - const options = deviceTypesService.getDeviceTypesOptions( token); return { streamed:{ - result: { - list: result, - options: options - } + result: handle() } }; }; \ No newline at end of file diff --git a/src/routes/auth/login/+page.server.ts b/src/routes/auth/login/+page.server.ts index c82a95e..d15a7d9 100644 --- a/src/routes/auth/login/+page.server.ts +++ b/src/routes/auth/login/+page.server.ts @@ -2,14 +2,13 @@ import type { Actions } from './$types'; import { fail } from '@sveltejs/kit'; import { authService } from '$lib/api/services/authService.ts'; import { resolve } from '$app/paths'; -import { HttpError } from '$lib/api/httpClient.ts'; import { ApiError } from '$lib/types/api.ts'; import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts'; export const actions:Actions = { - default: async ({ request,cookies ,url}) => { + default: async ({ request,cookies ,url ,locals}) => { const formData = await request.formData(); const username = formData.get('username'); const password = formData.get('password'); @@ -44,7 +43,7 @@ export const actions:Actions = { try{ - const response = await authService.login({username,password}); + const response = await authService.login(locals.api,{username,password}); cookies.set(COOKIE_TOKEN_KEY,`${response.tokenHead} ${response.token}`,{ path: '/', diff --git a/src/routes/layout.css b/src/routes/layout.css index 762110a..186ff19 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -1,4 +1,9 @@ -@import 'tailwindcss'; +@import "tailwindcss"; @plugin "daisyui"{ - themes: all; + themes: light , dark , cupcake , bumblebee , emerald , corporate + , synthwave , retro , cyberpunk , valentine , halloween , garden + , forest , aqua , lofi , pastel , fantasy , wireframe --default , black --prefersdark + , luxury , dracula , cmyk , autumn , business , acid , lemonade + , night , coffee , winter , dim , nord , sunset , caramellatte + , abyss , silk;; } \ No newline at end of file