From ab43a9a140d92e8cf52120987b29f09a1b533aa5 Mon Sep 17 00:00:00 2001 From: Chaos Date: Mon, 1 Dec 2025 17:27:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(users):=20=E5=AE=9E=E7=8E=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加用户列表分页、搜索和角色筛选功能 - 实现用户批量选择与操作(删除/封禁) - 引入ofetch库优化API请求处理 - 添加表格加载状态和错误处理组件 - 更新图标组件属性以支持新特性 - 修复页面跳转状态码问题(302改为303) - 优化用户表格UI展示细节与交互体验 --- package-lock.json | 45 +- package.json | 3 +- src/lib/api/httpClient.ts | 153 ++----- src/lib/api/services/roleService.ts | 2 + src/lib/api/services/userService.ts | 8 +- src/lib/components/ToastContainer.svelte | 2 +- .../components/error/TableLoadingError.svelte | 22 + .../components/layout/app/AppHeader.svelte | 2 +- .../components/layout/app/AppSidebar.svelte | 8 +- .../loading/TableLoadingState.svelte | 4 + src/lib/components/table/DevicesTable.svelte | 2 +- src/lib/components/table/UserTableOld.svelte | 2 +- src/lib/components/table/UsersTable.svelte | 400 +++++++++++++----- .../app/settings/auth/users/+page.server.ts | 14 +- .../app/settings/auth/users/+page.svelte | 44 +- src/routes/auth/login/+page.svelte | 2 +- 16 files changed, 442 insertions(+), 271 deletions(-) create mode 100644 src/lib/components/error/TableLoadingError.svelte create mode 100644 src/lib/components/loading/TableLoadingState.svelte diff --git a/package-lock.json b/package-lock.json index 0acde90..b7409d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "name": "chaos-it", "version": "0.0.1", "dependencies": { - "daisyui": "^5.5.5" + "daisyui": "^5.5.5", + "ofetch": "^1.5.1" }, "devDependencies": { "@eslint/compat": "^1.4.0", @@ -1473,6 +1474,7 @@ "integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1512,6 +1514,7 @@ "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", @@ -1844,6 +1847,7 @@ "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1894,6 +1898,7 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -2112,6 +2117,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2396,6 +2402,12 @@ "node": ">=0.10.0" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2488,6 +2500,7 @@ "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", @@ -3512,6 +3525,23 @@ "license": "MIT", "optional": true }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -3608,6 +3638,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3635,6 +3666,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3768,6 +3800,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3784,6 +3817,7 @@ "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" @@ -4510,6 +4544,7 @@ "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", @@ -4707,6 +4742,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4739,6 +4775,12 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", @@ -4776,6 +4818,7 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/package.json b/package.json index 440568a..417d77a 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "vite": "^7.1.10" }, "dependencies": { - "daisyui": "^5.5.5" + "daisyui": "^5.5.5", + "ofetch": "^1.5.1" } } diff --git a/src/lib/api/httpClient.ts b/src/lib/api/httpClient.ts index 46ed33a..4086328 100644 --- a/src/lib/api/httpClient.ts +++ b/src/lib/api/httpClient.ts @@ -1,11 +1,9 @@ +import { ofetch, type FetchOptions, type SearchParameters } from 'ofetch'; +import { log } from '$lib/log'; -import { browser } from '$app/environment'; -import { log } from '$lib/log.ts'; - - -type JsonValue = string | number | boolean | null | JsonObject | JsonValue[]; -interface JsonObject { [key: string]: JsonValue } -type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; +// 1. 定义更安全的类型,替代 any +type QueryParams = SearchParameters; // 使用 ofetch 内置的查询参数类型,或者自定义 Record +type RequestBody = Record | FormData | unknown[]; // 替代 any,使用 unknown export interface ApiResult { code: number; @@ -13,130 +11,41 @@ export interface ApiResult { data: T; } -interface RequestOptions extends Omit { - body?: JsonObject | FormData | object; - customFetch?: typeof fetch; -} +const BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api'; -const API_BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api'; - -const normalizeHeaders = (headers?: HeadersInit): Record => { - const result: Record = {}; - if (!headers) return result; - - if (headers instanceof Headers) { - headers.forEach((value, key) => { - result[key.toLowerCase()] = value; +// 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 + query: options.query }); - } else if (Array.isArray(headers)) { - headers.forEach(([key, value]) => { - result[key.toLowerCase()] = value; - }); - } else { - Object.keys(headers).forEach(key => { - const value = (headers as Record)[key]; - if (value !== undefined && value !== null) { - result[key.toLowerCase()] = value; - } + }, + onResponseError({ request, response }) { + log.error(`[API] Error ${request}`, { + status: response.status, + data: response._data as unknown }); } - return result; -}; +}); -export class HttpError extends Error { - public status: number; - public details: JsonValue | string; - - constructor(message: string, status: number, details: JsonValue | string) { - super(message); - this.name = 'HttpError'; - this.status = status; - this.details = details; - } -} - -const httpRequest = async ( - url: string, - method: HttpMethod, - options: RequestOptions = {} -): Promise> => { - const fullUrl = url.startsWith('http') ? url : `${API_BASE_URL}${url}`; - - const { body, headers, customFetch, ...rest } = options; - const fetchImpl = customFetch || fetch; - - const requestHeaders: Record = normalizeHeaders(headers); - let requestBody: BodyInit | undefined; - - const canHaveBody = method !== 'GET' && method !== 'HEAD'; - - if (canHaveBody) { - if (body instanceof FormData) { - requestBody = body; - delete requestHeaders['content-type']; - } else if (body) { - requestHeaders['content-type'] = 'application/json'; - requestBody = JSON.stringify(body); - } - } - - try { - if (browser) { - console.debug(`[API] ${method} ${fullUrl}`, { body: requestBody, headers: requestHeaders }); - } - - const response = await fetchImpl(fullUrl, { - method, - headers: requestHeaders, - body: canHaveBody ? requestBody : undefined, - ...rest - }); - - if (!response.ok) { - let errorDetail; - try { - errorDetail = await response.json(); - } catch (e) { - log.error('Error parsing JSON response:', e) - errorDetail = await response.text(); - } - const message = `HTTP Error ${response.status} (${response.statusText})`; - console.warn(message, errorDetail); - throw new HttpError(message, response.status, errorDetail); - } - - // 处理空响应 - if (response.status === 204) { - return { code: 200, msg: 'OK', data: null as unknown as T }; - } - - const contentType = response.headers.get('Content-Type'); - if (contentType && contentType.includes('application/json')) { - const res = await response.json(); - return res as ApiResult; - } - - return { code: 200, msg: 'OK', data: null as unknown as T }; - - } catch (err) { - console.error(`API Request Failed to ${fullUrl}:`, err); - throw err; - } -}; +// 3. 辅助类型:剔除我们手动处理的属性,并强制 responseType 为 'json' +type AppFetchOptions = Omit, 'method' | 'body' | 'query'>; export const api = { - get: (url: string, options?: RequestOptions) => - httpRequest(url, 'GET', options), + get: (url: string, query?: QueryParams, options?: AppFetchOptions) => + client>(url, { ...options, method: 'GET', query }), - post: (url: string, body: JsonObject | FormData, options?: RequestOptions) => - httpRequest(url, 'POST', { ...options, body }), + post: (url: string, body?: RequestBody, options?: AppFetchOptions) => + client>(url, { ...options, method: 'POST', body }), - put: (url: string, body: JsonObject | FormData, options?: RequestOptions) => - httpRequest(url, 'PUT', { ...options, body }), + put: (url: string, body?: RequestBody, options?: AppFetchOptions) => + client>(url, { ...options, method: 'PUT', body }), - delete: (url: string, options?: RequestOptions) => - httpRequest(url, 'DELETE', options), + patch: (url: string, body?: RequestBody, options?: AppFetchOptions) => + client>(url, { ...options, method: 'PATCH', body }), - patch: (url: string, body: JsonObject | FormData, options?: RequestOptions) => - httpRequest(url, 'PATCH', { ...options, body }), + 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/roleService.ts b/src/lib/api/services/roleService.ts index 1138e1a..727deec 100644 --- a/src/lib/api/services/roleService.ts +++ b/src/lib/api/services/roleService.ts @@ -1,11 +1,13 @@ import { api } 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', {headers: {Authorization: `${token}`}}); if (response.code != 200 || !response.data){ + log.error(response.msg); throw new Error(response.msg); } return response.data; diff --git a/src/lib/api/services/userService.ts b/src/lib/api/services/userService.ts index 978269e..7e0b3d1 100644 --- a/src/lib/api/services/userService.ts +++ b/src/lib/api/services/userService.ts @@ -10,10 +10,16 @@ export const userService = { } return response.data; }, - getAllUsers: async ({ page, size,token}: { page: number, size: number, token:string}) => { + getAllUsers: async ({ page, size,token , keyword, roleId}: { page: number, size: number, token:string , keyword?: string, roleId?: number}) => { const formData = new FormData(); formData.append('pageNum', page.toString()); formData.append('pageSize', size.toString()); + if ( keyword){ + formData.append('keyword', keyword); + } + if ( roleId){ + formData.append('roleId', roleId.toString()); + } const response = await api.get>( '/users', { diff --git a/src/lib/components/ToastContainer.svelte b/src/lib/components/ToastContainer.svelte index afdb449..e740547 100644 --- a/src/lib/components/ToastContainer.svelte +++ b/src/lib/components/ToastContainer.svelte @@ -22,7 +22,7 @@ transition:fly={{ x: 100, duration: 300 }} class="alert bg-base-100 text-base-content border-0 shadow-base-300/50 shadow-lg min-w-[200px] flex justify-start" > - + {t.message} {/each} diff --git a/src/lib/components/error/TableLoadingError.svelte b/src/lib/components/error/TableLoadingError.svelte new file mode 100644 index 0000000..3eb5328 --- /dev/null +++ b/src/lib/components/error/TableLoadingError.svelte @@ -0,0 +1,22 @@ + + + +
+ + + +

加载失败

+

{message}

+ +
\ No newline at end of file diff --git a/src/lib/components/layout/app/AppHeader.svelte b/src/lib/components/layout/app/AppHeader.svelte index f822a07..2e86c27 100644 --- a/src/lib/components/layout/app/AppHeader.svelte +++ b/src/lib/components/layout/app/AppHeader.svelte @@ -13,7 +13,7 @@ - + diff --git a/src/lib/components/layout/app/AppSidebar.svelte b/src/lib/components/layout/app/AppSidebar.svelte index 79a51e8..d50e87f 100644 --- a/src/lib/components/layout/app/AppSidebar.svelte +++ b/src/lib/components/layout/app/AppSidebar.svelte @@ -136,7 +136,7 @@ {#if item.icon} - + {/if} {item.label} @@ -146,7 +146,7 @@ handleClick(subItem)}> {#if subItem.icon} - + {/if} {subItem.label} @@ -159,7 +159,7 @@ {#if childItem.icon} - + {:else}
@@ -179,7 +179,7 @@ {#if item.icon} - + {/if} {item.label} diff --git a/src/lib/components/loading/TableLoadingState.svelte b/src/lib/components/loading/TableLoadingState.svelte new file mode 100644 index 0000000..a9cc2d6 --- /dev/null +++ b/src/lib/components/loading/TableLoadingState.svelte @@ -0,0 +1,4 @@ +
+ +

正在加载数据...

+
\ No newline at end of file diff --git a/src/lib/components/table/DevicesTable.svelte b/src/lib/components/table/DevicesTable.svelte index 03b046f..4e832f3 100644 --- a/src/lib/components/table/DevicesTable.svelte +++ b/src/lib/components/table/DevicesTable.svelte @@ -55,7 +55,7 @@ -{#await data.streamed.userList} -
-
- +
+ {#await promiseCombined} + + {:then [users, rolesOptions]} + +
+
-

加载中...

-
-{:then result} - {#await data.streamed.rolesOptions} -
-
+ {:catch error} -
-

加载中...

-
- {:then rolesOptions} - - {:catch err} -

出错了: {err.message}

+ {/await} -{:catch err} -

出错了: {err.message}

-{/await} \ No newline at end of file +
+ + diff --git a/src/routes/auth/login/+page.svelte b/src/routes/auth/login/+page.svelte index 30b0a8c..439814e 100644 --- a/src/routes/auth/login/+page.svelte +++ b/src/routes/auth/login/+page.svelte @@ -55,7 +55,7 @@

- + IT DTMS登录