feat(web): 登录、注册功能

This commit is contained in:
R524809
2026-09-03 16:06:13 +08:00
parent 35fcf8776e
commit ffa86f8d55
15 changed files with 5026 additions and 5757 deletions
+48 -8
View File
@@ -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;