feat(web): 登录、注册功能
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import type { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
|
||||
import type {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
InternalAxiosRequestConfig,
|
||||
} from 'axios';
|
||||
import { envConfig } from '@/config/env';
|
||||
import { authToken } from '@/services/authToken';
|
||||
|
||||
@@ -34,10 +39,11 @@ let refreshTask: Promise<string | null> | null = null;
|
||||
/** 并发 401 只触发一次刷新请求;刷新依赖 HttpOnly refresh cookie */
|
||||
function refreshAccessToken(): Promise<string | null> {
|
||||
refreshTask ??= apiClient
|
||||
.post<{ access_token: string }>('/auth/refresh')
|
||||
.post('/auth/refresh')
|
||||
.then((res) => {
|
||||
authToken.set(res.data.access_token);
|
||||
return res.data.access_token;
|
||||
const token = unwrapEnvelope<{ access_token: string }>(res).access_token;
|
||||
authToken.set(token);
|
||||
return token;
|
||||
})
|
||||
.catch(() => {
|
||||
authToken.clear();
|
||||
@@ -72,18 +78,31 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 统一解包强制响应信封:成功为 { data: T },204 No Content 无响应体
|
||||
* (docs/api/conventions.md §3.1)。服务层泛型 T 指业务负载本身;
|
||||
* 未来列表接口({ data, meta })在此扩展专门的解包方法以保留 meta。
|
||||
*/
|
||||
function unwrapEnvelope<T>(response: AxiosResponse): T {
|
||||
const body = response.data as { data?: unknown } | undefined;
|
||||
if (body === null || body === undefined) {
|
||||
return undefined as T;
|
||||
}
|
||||
return body.data as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T = unknown>(url: string, config?: AxiosRequestConfig) =>
|
||||
apiClient.get<T>(url, config).then((res) => res.data),
|
||||
apiClient.get<T>(url, config).then((res) => unwrapEnvelope<T>(res)),
|
||||
|
||||
post: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
|
||||
apiClient.post<T>(url, data, config).then((res) => res.data),
|
||||
apiClient.post<T>(url, data, config).then((res) => unwrapEnvelope<T>(res)),
|
||||
|
||||
patch: <T = unknown>(url: string, data?: unknown, config?: AxiosRequestConfig) =>
|
||||
apiClient.patch<T>(url, data, config).then((res) => res.data),
|
||||
apiClient.patch<T>(url, data, config).then((res) => unwrapEnvelope<T>(res)),
|
||||
|
||||
delete: <T = unknown>(url: string, config?: AxiosRequestConfig) =>
|
||||
apiClient.delete<T>(url, config).then((res) => res.data),
|
||||
apiClient.delete<T>(url, config).then((res) => unwrapEnvelope<T>(res)),
|
||||
};
|
||||
|
||||
/** 统一错误格式为 { error: { code, message, request_id } }(docs/api/conventions.md),提取可展示信息 */
|
||||
@@ -99,4 +118,25 @@ export function apiErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
export interface ApiErrorDetail {
|
||||
field: string;
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** 提取错误信封中的字段级明细 details: [{ field, code, message }](docs/api/conventions.md §4) */
|
||||
export function apiErrorDetails(error: unknown): ApiErrorDetail[] {
|
||||
if (!axios.isAxiosError(error)) return [];
|
||||
const details = (error.response?.data as { error?: { details?: unknown } } | undefined)?.error
|
||||
?.details;
|
||||
if (!Array.isArray(details)) return [];
|
||||
return details.filter(
|
||||
(item): item is ApiErrorDetail =>
|
||||
!!item &&
|
||||
typeof item === 'object' &&
|
||||
typeof (item as ApiErrorDetail).field === 'string' &&
|
||||
typeof (item as ApiErrorDetail).message === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export default apiClient;
|
||||
|
||||
Reference in New Issue
Block a user