feat(users): 实现用户管理页面功能增强

- 添加用户列表分页、搜索和角色筛选功能
- 实现用户批量选择与操作(删除/封禁)
- 引入ofetch库优化API请求处理
- 添加表格加载状态和错误处理组件
- 更新图标组件属性以支持新特性
- 修复页面跳转状态码问题(302改为303)
- 优化用户表格UI展示细节与交互体验
This commit is contained in:
Chaos
2025-12-01 17:27:02 +08:00
parent bd00e54acd
commit ab43a9a140
16 changed files with 442 additions and 271 deletions

View File

@@ -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<string, string | number | boolean>
type RequestBody = Record<string, unknown> | FormData | unknown[]; // 替代 any使用 unknown
export interface ApiResult<T> {
code: number;
@@ -13,130 +11,41 @@ export interface ApiResult<T> {
data: T;
}
interface RequestOptions extends Omit<RequestInit, 'method' | 'body'> {
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<string, string> => {
const result: Record<string, string> = {};
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<string, string>)[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 <T>(
url: string,
method: HttpMethod,
options: RequestOptions = {}
): Promise<ApiResult<T>> => {
const fullUrl = url.startsWith('http') ? url : `${API_BASE_URL}${url}`;
const { body, headers, customFetch, ...rest } = options;
const fetchImpl = customFetch || fetch;
const requestHeaders: Record<string, string> = 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<T>;
}
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<FetchOptions<'json'>, 'method' | 'body' | 'query'>;
export const api = {
get: <T>(url: string, options?: RequestOptions) =>
httpRequest<T>(url, 'GET', options),
get: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'GET', query }),
post: <T>(url: string, body: JsonObject | FormData, options?: RequestOptions) =>
httpRequest<T>(url, 'POST', { ...options, body }),
post: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'POST', body }),
put: <T>(url: string, body: JsonObject | FormData, options?: RequestOptions) =>
httpRequest<T>(url, 'PUT', { ...options, body }),
put: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'PUT', body }),
delete: <T>(url: string, options?: RequestOptions) =>
httpRequest<T>(url, 'DELETE', options),
patch: <T>(url: string, body?: RequestBody, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'PATCH', body }),
patch: <T>(url: string, body: JsonObject | FormData, options?: RequestOptions) =>
httpRequest<T>(url, 'PATCH', { ...options, body }),
delete: <T>(url: string, query?: QueryParams, options?: AppFetchOptions) =>
client<ApiResult<T>>(url, { ...options, method: 'DELETE', query })
};