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==", "integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@standard-schema/spec": "^1.0.0", "@standard-schema/spec": "^1.0.0",
"@sveltejs/acorn-typescript": "^1.0.5", "@sveltejs/acorn-typescript": "^1.0.5",
@@ -1514,7 +1513,6 @@
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
"debug": "^4.4.1", "debug": "^4.4.1",
@@ -1847,7 +1845,6 @@
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
@@ -1898,7 +1895,6 @@
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0", "@typescript-eslint/types": "8.47.0",
@@ -2117,7 +2113,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2500,7 +2495,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -3638,7 +3632,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3666,7 +3659,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -3800,7 +3792,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -3817,7 +3808,6 @@
"integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"prettier": "^3.0.0", "prettier": "^3.0.0",
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.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==", "integrity": "sha512-d1R+3pFa39LXoHCsxHmV//D2pSFZlEMlnxCVQ54TlrQv+4o5pewJO0/Pc5MUp+j71PJrOrPJHTvREZJHn+ymDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.4", "@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -4742,7 +4731,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -4818,7 +4806,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.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 // See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces // for information about these interfaces
import type { ApiClient } from '$lib/api/httpClient.ts';
declare global { declare global {
namespace App { namespace App {
interface User { interface User {
@@ -16,6 +18,9 @@ declare global {
interface pageData { interface pageData {
user: User | null; 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 { parseJwt } from '$lib/utils/tokenUtils.ts';
import type { JwtPayload } from '$lib/types/auth.ts'; import type { JwtPayload } from '$lib/types/auth.ts';
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.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}) =>{ export const handle: Handle = async ({ event, resolve}) =>{
const authorization = event.cookies.get(COOKIE_TOKEN_KEY); const authorization = event.cookies.get(COOKIE_TOKEN_KEY);
event.locals.api = createApi(authorization);
if (authorization){ if (authorization){
const split = authorization?.split(' '); const split = authorization?.split(' ');
const token = split[1]; const token = split[1];
const jwt = parseJwt<JwtPayload>(token); const jwt = parseJwt<JwtPayload>(token);
if (jwt){ if (jwt){
event.locals.user = { event.locals.user = {
id: jwt.userId, id: jwt.userId,
username: jwt.sub, username: jwt.sub,
@@ -19,8 +24,10 @@ export const handle: Handle = async ({ event, resolve}) =>{
roles: jwt.authorities roles: jwt.authorities
} }
} }
}else if(event.url.pathname.startsWith('/app')){
throw redirect(303, '/auth/login');
} }
return resolve(event); return resolve(event);
} }

View File

@@ -1,10 +1,9 @@
import { ofetch, type FetchOptions, type SearchParameters } from 'ofetch'; import { ofetch, type FetchOptions, type SearchParameters } from 'ofetch';
import { log } from '$lib/log'; import { log } from '$lib/log';
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
// 1. 定义更安全的类型,替代 any
type QueryParams = SearchParameters; 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> { export interface ApiResult<T> {
code: number; code: number;
@@ -14,41 +13,61 @@ export interface ApiResult<T> {
const BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api'; const BASE_URL = import.meta.env.VITE_PUBLIC_API_URL || 'http://localhost:18888/api';
// 2. 指定 create 的默认类型为 json export type ApiClient = ReturnType<typeof createApi>;
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
});
}
});
// 3. 辅助类型:剔除我们手动处理的属性,并强制 responseType 为 'json' export const createApi = (token?: string) => {
type AppFetchOptions = Omit<FetchOptions<'json'>, 'method' | 'body' | 'query'>; 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: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) => body: options.body as unknown,
client<ApiResult<T>>(url, { ...options, method: 'GET', query }), 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: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) => return {
client<ApiResult<T>>(url, { ...options, method: 'POST', body }), get: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'GET', query }),
put: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) => // 关键修复点:
client<ApiResult<T>>(url, { ...options, method: 'PUT', 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>
}),
patch: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) => put: <T, B = RequestBody>(url: string, body?: B, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'PATCH', body }), client<ApiResult<T>>(url, {
...options,
method: 'PUT',
body: body as unknown as Record<string, unknown>
}),
delete: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) => patch: <T, B = RequestBody>(url: string, body?: B, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'DELETE', query }) 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 type { AuthResponse, LoginPayload } from '$lib/types/auth';
import { ApiError } from '$lib/types/api.ts'; import { ApiError } from '$lib/types/api.ts';
import type { ApiClient } from '$lib/api/httpClient.ts';
export const authService = { export const authService = {
/** /**
* 登录流程 * 登录流程
*/ */
login: async (payload: LoginPayload): Promise<AuthResponse> => { login: async (api: ApiClient,payload: LoginPayload): Promise<AuthResponse> => {
// 1. 调用登录接口
const response = await api.post<AuthResponse>('/auth/login', payload); const response = await api.post<AuthResponse>('/auth/login', payload);
if (response.code !== 200 || !response.data) { if (response.code !== 200 || !response.data) {
@@ -21,12 +21,10 @@ export const authService = {
/** /**
* 登出流程 * 登出流程
*/ */
logout: async () => { logout: async (api: ApiClient) => {
try { try {
// Optionally call the backend logout endpoint
await api.post('/auth/logout', {}); await api.post('/auth/logout', {});
} catch (error) { } catch (error) {
// Even if the backend call fails, we still want to clear local state
console.warn('Logout API call failed:', error); 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 { PageResult } from '$lib/types/dataTable.ts';
import type { CreateDeviceRequest, DeviceResponse } from '$lib/types/api.ts'; import type { CreateDeviceRequest, DeviceResponse } from '$lib/types/api.ts';
import type { JsonValue } from '$lib/types/http.ts';
export const deviceService = { export const deviceService = {
getAllDevices: async ({ page, size,type,keyword,token}:{ getAllDevices: async (api:ApiClient,{ page, size,type,keyword}:{
page: number, page: number,
size: number, size: number,
type?: number, type?: number,
keyword?: string, 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, const queryParams: Record<string, string | number> = {
headers:{Authorization: `${token}`} 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){ if (result.code != 200 || !result.data){
throw new Error(result.msg); throw new Error(result.msg);
@@ -33,12 +26,9 @@ export const deviceService = {
return result.data; return result.data;
}, },
createDevice: async (device: CreateDeviceRequest,token:string) => { createDevice: async (api: ApiClient,device: CreateDeviceRequest) => {
const result = await api.post<DeviceResponse>('/devices',{ const result = await api.post<DeviceResponse>('/devices', device);
body: device,
headers:{Authorization: `${token}`}
});
if (result.code != 200 || !result.data){ if (result.code != 200 || !result.data){
throw new Error(result.msg); 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 { Options } from '$lib/types/api.ts';
import type { ApiClient } from '$lib/api/httpClient.ts';
export const deviceTypesService = { export const deviceTypesService = {
getDeviceTypesOptions: async (token:string) => { getDeviceTypesOptions: async (api:ApiClient) => {
const result = await api.get<Options[]>('/device-types/options',{ const result = await api.get<Options[]>('/device-types/options',undefined);
headers:{Authorization: `${token}`}
});
if (result.code != 200 || !result.data){ if (result.code != 200 || !result.data){
throw new Error(result.msg); 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 type { Options } from '$lib/types/api.ts';
import { log } from '$lib/log.ts'; import { log } from '$lib/log.ts';
export const roleService = { export const roleService = {
getRolesOptions: async (token:string) => { getRolesOptions: async (api:ApiClient) => {
const response = await api.get<Options[]>('/roles/options',undefined, {headers: {Authorization: `${token}`}}); const response = await api.get<Options[]>('/roles/options',undefined);
if (response.code != 200 || !response.data){ if (response.code != 200 || !response.data){
log.error(response.msg); log.error(response.msg);
throw new 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 { UserProfile } from '$lib/types/user.ts';
import type { PageResult } from '$lib/types/dataTable.ts'; import type { PageResult } from '$lib/types/dataTable.ts';
import { type SearchParameters } from 'ofetch'; import { type SearchParameters } from 'ofetch';
@@ -7,14 +7,14 @@ import { type SearchParameters } from 'ofetch';
// 1. 定义更安全的类型,替代 any // 1. 定义更安全的类型,替代 any
type QueryParams = SearchParameters; type QueryParams = SearchParameters;
export const userService = { export const userService = {
getUserProfile: async (token:string) => { getUserProfile: async (api:ApiClient) => {
const response = await api.get<UserProfile>('/users/me',undefined, {headers: {Authorization: `${token}`}}); const response = await api.get<UserProfile>('/users/me',undefined);
if (response.code != 200 || !response.data){ if (response.code != 200 || !response.data){
throw new Error(response.msg); throw new Error(response.msg);
} }
return response.data; 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= { const params: QueryParams= {
pageNum: page, pageNum: page,
@@ -24,10 +24,7 @@ export const userService = {
} ; } ;
const response = await api.get<PageResult<UserProfile[]>>( const response = await api.get<PageResult<UserProfile[]>>(
'/users', '/users',
params, params);
{
headers: {Authorization: `${token}`}
});
if (response.code != 200 || !response.data){ if (response.code != 200 || !response.data){
throw new Error(response.msg); 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> <span class="loading loading-spinner loading-lg text-primary"></span>
<p class="text-base-content/70 mt-4 font-medium animate-pulse">正在加载数据...</p> <p class="text-base-content/70 mt-4 font-medium animate-pulse">正在加载数据...</p>
</div> </div>

View File

@@ -83,13 +83,12 @@
{ title: '用户组', width: 45 } { title: '用户组', width: 45 }
]; ];
</script> </script>
<div class="flex-1 overflow-y-auto">
<div class="overflow-x-auto flex-1 bg-base-100 h-full "> <div class="bg-base-100">
<table class="table "> <table class="table table-pin-rows">
<thead class="z-0"> <thead>
<tr> <tr>
<th class="w-12"> <th class="w-12 bg-base-100"> <label>
<label>
<input <input
type="checkbox" type="checkbox"
class="checkbox checkbox-sm" class="checkbox checkbox-sm"
@@ -98,64 +97,65 @@
onchange={toggleAll} onchange={toggleAll}
/> />
</label> </label>
</th>
{#each newRowTitles as item (item.title)}
<th style="width: {item.width}%" >{item.title}</th>
{/each}
</tr>
</thead>
<tbody>
{#each users.records as record (record.id)}
<tr class="hover">
<th>
<label>
<input
type="checkbox"
class="checkbox checkbox-sm"
checked={selectedIds.includes(record.id)}
onchange={() => toggleOne(record.id)}
/>
</label>
</th> </th>
<td class="font-mono text-xs opacity-70">{record.id}</td> {#each newRowTitles as item (item.title)}
<td class="font-bold">{record.username}</td> <th style="width: {item.width}%" class="bg-base-100">{item.title}</th>
<td>{record.nickname || '-'}</td> {/each}
<td>
<div class="avatar">
<div class="w-8 h-8 rounded-full bg-base-300 ring ring-base-200 ring-offset-base-100 ring-offset-2">
{#if record.avatar}
<img src={record.avatar} alt={record.username} />
{:else}
<span class="text-xs flex items-center justify-center h-full w-full uppercase">
{record.username.slice(0, 2)}
</span>
{/if}
</div>
</div>
</td>
<td>
<div class="flex flex-wrap gap-1">
{#each record.roles as role (role.id)}
<span class="badge badge-sm {role.id === 1 ? 'badge-primary' : 'badge-ghost'}">
{role.name}
</span>
{/each}
</div>
</td>
</tr> </tr>
{/each} </thead>
</tbody>
</table> <tbody class="w-full">
{#each users.records as record (record.id)}
<tr class="hover">
<th>
<label>
<input
type="checkbox"
class="checkbox checkbox-sm"
checked={selectedIds.includes(record.id)}
onchange={() => toggleOne(record.id)}
/>
</label>
</th>
<td class="font-mono text-xs opacity-70">{record.id}</td>
<td class="font-bold">{record.username}</td>
<td>{record.nickname || '-'}</td>
<td>
<div class="avatar">
<div class="w-8 h-8 rounded-full bg-base-300 ring ring-base-200 ring-offset-base-100 ring-offset-2">
{#if record.avatar}
<img src={record.avatar} alt={record.username} />
{:else}
<span class="text-xs flex items-center justify-center h-full w-full uppercase">
{record.username.slice(0, 2)}
</span>
{/if}
</div>
</div>
</td>
<td>
<div class="flex flex-wrap gap-1">
{#each record.roles as role (role.id)}
<span class="badge badge-sm {role.id === 1 ? 'badge-primary' : 'badge-ghost'}">
{role.name}
</span>
{/each}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
{#if users.records.length === 0}
<div class="flex flex-col items-center justify-center p-10 text-base-content/50">
<Icon id="search-off" size="48" />
<p class="mt-2">未找到匹配的用户</p>
</div>
{/if}
</div>
{#if users.records.length === 0}
<div class="flex flex-col items-center justify-center p-10 text-base-content/50">
<Icon id="search-off" size="48" />
<p class="mt-2">未找到匹配的用户</p>
</div>
{/if}
</div> </div>
{#if users.total > 0} {#if users.total > 0}
<div class="border-t border-base-200 p-4 flex items-center justify-between bg-base-100 "> <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"> <div class="text-sm text-base-content/70">

View File

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

View File

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

View File

@@ -1,20 +1,15 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { userService } from '$lib/api/services/userService.ts'; 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 { roleService } from '$lib/api/services/roleService.ts';
import { log } from '$lib/log.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 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 keyword = url.searchParams.get('q') || undefined;
const role = Number(url.searchParams.get('role')) || undefined; const role = Number(url.searchParams.get('role')) || undefined;
@@ -22,13 +17,13 @@ export const load:PageServerLoad = async ({ cookies ,url }) => {
const getRoles = async() => { const getRoles = async() => {
return await roleService.getRolesOptions(token); return await roleService.getRolesOptions(locals.api);
} }
const getUserList = async() => { const getUserList = async() => {
await new Promise(resolve => setTimeout(resolve, 1000)); 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 { return {

View File

@@ -119,7 +119,7 @@
</div> </div>
</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} {#await data.streamed.userList}
<TableLoadingState /> <TableLoadingState />
{:then users} {:then users}

View File

@@ -1,25 +1,24 @@
import type { PageServerLoad } from './$types'; 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 { deviceService } from '$lib/api/services/deviceService.ts';
import { deviceTypesService } from '$lib/api/services/deviceTypesService.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) { const result = deviceService.getAllDevices(locals.api,{ page: 1, size: 10 });
throw redirect(302, '/auth/login'); 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 { return {
streamed:{ streamed:{
result: { result: handle()
list: result,
options: options
}
} }
}; };
}; };

View File

@@ -2,14 +2,13 @@ import type { Actions } from './$types';
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
import { authService } from '$lib/api/services/authService.ts'; import { authService } from '$lib/api/services/authService.ts';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { HttpError } from '$lib/api/httpClient.ts';
import { ApiError } from '$lib/types/api.ts'; import { ApiError } from '$lib/types/api.ts';
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts'; import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
export const actions:Actions = { export const actions:Actions = {
default: async ({ request,cookies ,url}) => { default: async ({ request,cookies ,url ,locals}) => {
const formData = await request.formData(); const formData = await request.formData();
const username = formData.get('username'); const username = formData.get('username');
const password = formData.get('password'); const password = formData.get('password');
@@ -44,7 +43,7 @@ export const actions:Actions = {
try{ 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}`,{ cookies.set(COOKIE_TOKEN_KEY,`${response.tokenHead} ${response.token}`,{
path: '/', path: '/',

View File

@@ -1,4 +1,9 @@
@import 'tailwindcss'; @import "tailwindcss";
@plugin "daisyui"{ @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;;
} }