fix: 修复前端多项问题并完善认证流程
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 15m39s

- 修复 AuthContext 硬编码 localhost:8080,改用 api.ts 统一的 API_BASE_URL(走环境变量/代理,生产可部署)
- 修复注册页发送验证码绕过 api.ts 直接 raw fetch 的问题,改用统一的 sendVerificationCode
- 修复 SliderCaptcha 验证成功后 captchaId 未透传导致注册人机验证失效的断链
- 修复头像上传契约:预签名URL方式(/user/avatar/upload-url)改为直传(/user/avatar/upload)
- 移除后端不支持的'设置活跃角色'功能(/profile/:uuid/activate)及相关死代码
- 修复分页字段不一致:新增 normalizePaginatedData 兼容后端 per_page 响应
- 强化邮箱正则校验(RFC 标准,禁止连续点/首尾点,TLD>=2字母)
- 移除首页指向无效 /api 页面的'查看API文档'按钮
- 新增 /register /login /signup 重定向路由,统一指向 /auth?mode=
- 同步 API文档.md
This commit is contained in:
2026-07-08 17:36:09 +08:00
parent fdd1d0c17b
commit 268344c357
11 changed files with 927 additions and 1026 deletions

View File

@@ -24,7 +24,7 @@ export interface Profile {
name: string;
skin_id?: number;
cape_id?: number;
is_active: boolean;
is_active?: boolean;
last_used_at?: string;
created_at: string;
updated_at: string;
@@ -38,6 +38,32 @@ export interface PaginatedResponse<T> {
total_pages: number;
}
// 后端原始分页响应结构(统一返回 { list, total, page, per_page },不含 total_pages/page_size
type RawPaginatedData<T> = {
list?: T[];
total?: number;
page?: number;
per_page?: number;
page_size?: number;
};
// 在前端补齐 page_size 与 total_pages由 total / page_size 向上取整),以便 UI 分页使用
function normalizePaginatedData<T>(raw: RawPaginatedData<T>): PaginatedResponse<T> {
const list = raw.list ?? [];
const total = raw.total ?? 0;
const page = raw.page ?? 1;
const page_size = raw.per_page ?? raw.page_size ?? 20;
const total_pages = page_size > 0 ? Math.max(1, Math.ceil(total / page_size)) : 1;
return { list, total, page, page_size, total_pages };
}
// 后端错误响应的 data 可能不是分页结构code !== 200用此类型宽松接收
type RawApiResponse<T> = {
code: number;
message: string;
data: T;
};
export interface ApiResponse<T> {
code: number;
message: string;
@@ -75,7 +101,11 @@ export async function searchTextures(params: {
},
});
return response.json();
const result: RawApiResponse<RawPaginatedData<Texture>> = await response.json();
if (result.code === 200 && result.data) {
return { ...result, data: normalizePaginatedData(result.data) };
}
return result as ApiResponse<PaginatedResponse<Texture>>;
}
// 获取材质详情
@@ -114,7 +144,11 @@ export async function getMyTextures(params: {
headers: getAuthHeaders(),
});
return response.json();
const result: RawApiResponse<RawPaginatedData<Texture>> = await response.json();
if (result.code === 200 && result.data) {
return { ...result, data: normalizePaginatedData(result.data) };
}
return result as ApiResponse<PaginatedResponse<Texture>>;
}
// 获取用户收藏的材质列表
@@ -131,7 +165,11 @@ export async function getFavoriteTextures(params: {
headers: getAuthHeaders(),
});
return response.json();
const result: RawApiResponse<RawPaginatedData<Texture>> = await response.json();
if (result.code === 200 && result.data) {
return { ...result, data: normalizePaginatedData(result.data) };
}
return result as ApiResponse<PaginatedResponse<Texture>>;
}
// 获取用户档案列表
@@ -156,10 +194,11 @@ export async function createProfile(name: string): Promise<ApiResponse<Profile>>
}
// 更新档案
// skin_id / cape_id: 显式传 null 表示移除当前皮肤的关联;传 number 表示设置;不传表示不修改。
export async function updateProfile(uuid: string, data: {
name?: string;
skin_id?: number;
cape_id?: number;
skin_id?: number | null;
cape_id?: number | null;
}): Promise<ApiResponse<Profile>> {
const response = await fetch(`${API_BASE_URL}/profile/${uuid}`, {
method: 'PUT',
@@ -180,16 +219,6 @@ export async function deleteProfile(uuid: string): Promise<ApiResponse<null>> {
return response.json();
}
// 设置活跃档案
export async function setActiveProfile(uuid: string): Promise<ApiResponse<{ message: string }>> {
const response = await fetch(`${API_BASE_URL}/profile/${uuid}/activate`, {
method: 'POST',
headers: getAuthHeaders(),
});
return response.json();
}
// 获取用户信息
export async function getUserProfile(): Promise<ApiResponse<{
id: number;
@@ -264,23 +293,39 @@ export async function uploadTexture(file: File, data: {
return response.json();
}
// 生成头像上传URL
export async function generateAvatarUploadUrl(fileName: string): Promise<ApiResponse<{
post_url: string;
form_data: Record<string, string>;
// 直接上传头像文件到后端multipart/form-data
// 后端接口 POST /user/avatar/upload,由后端负责写入对象存储并更新用户头像
export async function uploadAvatar(file: File): Promise<ApiResponse<{
avatar_url: string;
expires_in: number;
user: {
id: number;
username: string;
email: string;
avatar: string;
points: number;
role: string;
status: number;
last_login_at?: string;
created_at: string;
updated_at: string;
};
}>> {
const response = await fetch(`${API_BASE_URL}/user/avatar/upload-url`, {
const formData = new FormData();
formData.append('file', file);
const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
const response = await fetch(`${API_BASE_URL}/user/avatar/upload`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ file_name: fileName }),
headers: {
...(token && { Authorization: `Bearer ${token}` }),
},
body: formData,
});
return response.json();
}
// 更新头像URL
// 更新头像URL用于外部URL场景区别于文件直传
export async function updateAvatarUrl(avatarUrl: string): Promise<ApiResponse<{
id: number;
username: string;
@@ -323,3 +368,69 @@ export async function deleteTexture(id: number): Promise<ApiResponse<null>> {
return response.json();
}
// 更新材质信息(名称、描述、公开性)
// 对应后端 PUT /texture/{id}is_public 为布尔值时会更新该字段,会影响公开/隐藏状态
export async function updateTexture(id: number, data: {
name?: string;
description?: string;
is_public?: boolean;
}): Promise<ApiResponse<Texture>> {
const response = await fetch(`${API_BASE_URL}/texture/${id}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return response.json();
}
// 发送邮箱验证码
// type: register | reset_password | change_email
export async function sendVerificationCode(email: string, type: 'register' | 'reset_password' | 'change_email'): Promise<ApiResponse<null>> {
const response = await fetch(`${API_BASE_URL}/auth/send-code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, type }),
});
return response.json();
}
// 更换邮箱
export async function changeEmail(newEmail: string, verificationCode: string): Promise<ApiResponse<{
id: number;
username: string;
email: string;
avatar: string;
points: number;
role: string;
status: number;
last_login_at?: string;
created_at: string;
updated_at: string;
}>> {
const response = await fetch(`${API_BASE_URL}/user/change-email`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ new_email: newEmail, verification_code: verificationCode }),
});
return response.json();
}
// 重置密码(通过邮箱验证码)
// 对应后端 POST /auth/reset-password无需 JWT
export async function resetPassword(email: string, verificationCode: string, newPassword: string): Promise<ApiResponse<null>> {
const response = await fetch(`${API_BASE_URL}/auth/reset-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
verification_code: verificationCode,
new_password: newPassword,
}),
});
return response.json();
}