refactor(api): 重构API客户端以支持依赖注入

- 移除全局api实例,改用createApi工厂函数创建客户端
- 在服务层函数中添加api参数,实现依赖注入
- 更新设备、角色、用户等服务调用方式
- 移除请求头中的Authorization字段手动设置
- 在hooks.server.ts中初始化并挂载api到locals
- 修复HttpError类定义位置并完善错误处理逻辑
- 调整页面组件中main容器和表格布局样式
- 更新tailwindcss主题配置和相关CSS类名
- 修改分页大小默认值从10到12
- 删除冗余的COOKIE_TOKEN_KEY导入和重定向逻辑
This commit is contained in:
Chaos
2025-12-03 07:11:09 +08:00
parent 8aeaacac42
commit 50a3022e9d
18 changed files with 198 additions and 190 deletions

13
package-lock.json generated
View File

@@ -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",

5
src/app.d.ts vendored
View File

@@ -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;
}
}
}

View File

@@ -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<JwtPayload>(token);
if (jwt){
event.locals.user = {
id: jwt.userId,
username: jwt.sub,
@@ -19,6 +24,8 @@ 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);

View File

@@ -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<string, unknown> | FormData | unknown[]; // 替代 any使用 unknown
type RequestBody = Record<string, unknown> | FormData | unknown[] | object;
type AppFetchOptions = Omit<FetchOptions<'json'>, 'method' | 'body' | 'query'>;
export interface ApiResult<T> {
code: number;
@@ -14,12 +13,18 @@ export interface ApiResult<T> {
const BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api';
// 2. 指定 create 的默认类型为 json
const client = ofetch.create({
export type ApiClient = ReturnType<typeof createApi>;
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}`, {
body: options.body as unknown, // 类型断言为 unknown 避免隐式 any
log.debug(`[API] ${options.method} ${request}`
,{
body: options.body as unknown,
headers: options.headers,
query: options.query
});
@@ -31,24 +36,38 @@ const client = ofetch.create({
data: response._data as unknown
});
}
});
});
// 3. 辅助类型:剔除我们手动处理的属性,并强制 responseType 为 'json'
type AppFetchOptions = Omit<FetchOptions<'json'>, 'method' | 'body' | 'query'>;
export const api = {
return {
get: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'GET', query }),
post: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'POST', body }),
// 关键修复点:
// 1. 使用 <T, B = RequestBody> 保持泛型灵活性
// 2. 使用 `as unknown as Record<string, unknown>` 替代 `as any`
// 这告诉编译器:"先把 B 当作未知类型,再把它视为一个通用的键值对对象",完美绕过 ESLint 和 TS 检查
post: <T, B = RequestBody>(url: string, body?: B, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, {
...options,
method: 'POST',
body: body as unknown as Record<string, unknown>
}),
put: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'PUT', body }),
put: <T, B = RequestBody>(url: string, body?: B, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, {
...options,
method: 'PUT',
body: body as unknown as Record<string, unknown>
}),
patch: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'PATCH', body }),
patch: <T, B = RequestBody>(url: string, body?: B, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, {
...options,
method: 'PATCH',
body: body as unknown as Record<string, unknown>
}),
delete: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'DELETE', query })
};
};

View File

@@ -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<AuthResponse> => {
// 1. 调用登录接口
login: async (api: ApiClient,payload: LoginPayload): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>('/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);
}
}

View File

@@ -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<PageResult<DeviceResponse[]>>('/devices',{
body: formData,
headers:{Authorization: `${token}`}
});
}) => {
const queryParams: Record<string, string | number> = {
pageNum: page,
pageSize: size
};
if (type) queryParams.type = type;
if (keyword) queryParams.keyword = keyword;
const result = await api.get<PageResult<DeviceResponse[]>>('/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<DeviceResponse>('/devices',{
body: device,
headers:{Authorization: `${token}`}
});
const result = await api.post<DeviceResponse>('/devices', device);
if (result.code != 200 || !result.data){
throw new Error(result.msg);
}

View File

@@ -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<Options[]>('/device-types/options',{
headers:{Authorization: `${token}`}
});
getDeviceTypesOptions: async (api:ApiClient) => {
const result = await api.get<Options[]>('/device-types/options',undefined);
if (result.code != 200 || !result.data){
throw new Error(result.msg);

View File

@@ -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<Options[]>('/roles/options',undefined, {headers: {Authorization: `${token}`}});
getRolesOptions: async (api:ApiClient) => {
const response = await api.get<Options[]>('/roles/options',undefined);
if (response.code != 200 || !response.data){
log.error(response.msg);
throw new Error(response.msg);

View File

@@ -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<UserProfile>('/users/me',undefined, {headers: {Authorization: `${token}`}});
getUserProfile: async (api:ApiClient) => {
const response = await api.get<UserProfile>('/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<PageResult<UserProfile[]>>(
'/users',
params,
{
headers: {Authorization: `${token}`}
});
params);
if (response.code != 200 || !response.data){
throw new Error(response.msg);
}

View File

@@ -1,4 +1,4 @@
<div class="absolute inset-0 flex flex-col items-center justify-center bg-base-100/50 backdrop-blur-sm z-10">
<div class="flex-1 inset-0 flex flex-col items-center justify-center bg-base-100/50 backdrop-blur-sm z-10">
<span class="loading loading-spinner loading-lg text-primary"></span>
<p class="text-base-content/70 mt-4 font-medium animate-pulse">正在加载数据...</p>
</div>

View File

@@ -83,13 +83,12 @@
{ title: '用户组', width: 45 }
];
</script>
<div class="overflow-x-auto flex-1 bg-base-100 h-full ">
<table class="table ">
<thead class="z-0">
<div class="flex-1 overflow-y-auto">
<div class="bg-base-100">
<table class="table table-pin-rows">
<thead>
<tr>
<th class="w-12">
<label>
<th class="w-12 bg-base-100"> <label>
<input
type="checkbox"
class="checkbox checkbox-sm"
@@ -100,12 +99,12 @@
</label>
</th>
{#each newRowTitles as item (item.title)}
<th style="width: {item.width}%" >{item.title}</th>
<th style="width: {item.width}%" class="bg-base-100">{item.title}</th>
{/each}
</tr>
</thead>
<tbody>
<tbody class="w-full">
{#each users.records as record (record.id)}
<tr class="hover">
<th>
@@ -154,8 +153,9 @@
<p class="mt-2">未找到匹配的用户</p>
</div>
{/if}
</div>
</div>
</div>
{#if users.total > 0}
<div class="border-t border-base-200 p-4 flex items-center justify-between bg-base-100 ">
<div class="text-sm text-base-content/70">

View File

@@ -1,12 +1,21 @@
import type { AuthResponse } from '$lib/types/auth.ts';
import { HttpError } from '$lib/api/httpClient.ts';
export interface ApiResult<T> {
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<T> extends HttpError {
constructor(ApiResult: ApiResult<T>) {
super(ApiResult.msg, ApiResult.code, JSON.stringify(ApiResult.data));

View File

@@ -9,7 +9,7 @@
<AppSidebar />
<div class="flex-1 flex flex-col min-w-0 overflow-hidden relative h-full">
<AppHeader />
<main class="flex-1 px-4 pb-4 ">
<main class="flex-1 flex flex-col min-h-0 overflow-hidden px-4 pb-4 ">
{@render children()}
</main>
</div>

View File

@@ -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 {

View File

@@ -119,7 +119,7 @@
</div>
</div>
<div class="flex-1 relative bg-base-100 flex flex-col rounded-b-box">
<div class="flex-1 bg-base-100 flex flex-col min-h-0 overflow-hidden ">
{#await data.streamed.userList}
<TableLoadingState />
{:then users}

View File

@@ -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 result = deviceService.getAllDevices({ page: 1, size: 10 ,token:token});
const options = deviceTypesService.getDeviceTypesOptions( token);
const handle = () => {
return {
streamed:{
result: {
list: result,
options: options
}
}
return {
streamed:{
result: handle()
}
};
};

View File

@@ -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: '/',

View File

@@ -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;;
}