export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || '/api/v1'; export interface Texture { id: number; uploader_id: number; name: string; description?: string; type: 'SKIN' | 'CAPE'; url: string; hash: string; size: number; is_public: boolean; download_count: number; favorite_count: number; is_slim: boolean; status: number; created_at: string; updated_at: string; } export interface Profile { uuid: string; user_id: number; name: string; skin_id?: number; cape_id?: number; is_active?: boolean; last_used_at?: string; created_at: string; updated_at: string; } export interface PaginatedResponse { list: T[]; total: number; page: number; page_size: number; total_pages: number; } // 后端原始分页响应结构(统一返回 { list, total, page, per_page },不含 total_pages/page_size) type RawPaginatedData = { list?: T[]; total?: number; page?: number; per_page?: number; page_size?: number; }; // 在前端补齐 page_size 与 total_pages(由 total / page_size 向上取整),以便 UI 分页使用 function normalizePaginatedData(raw: RawPaginatedData): PaginatedResponse { 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 = { code: number; message: string; data: T; }; export interface ApiResponse { code: number; message: string; data: T; } // 获取认证头 function getAuthHeaders(): HeadersInit { const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null; return { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }), }; } // 搜索材质 export async function searchTextures(params: { keyword?: string; type?: 'SKIN' | 'CAPE'; public_only?: boolean; page?: number; page_size?: number; }): Promise>> { const queryParams = new URLSearchParams(); if (params.keyword) queryParams.append('keyword', params.keyword); if (params.type) queryParams.append('type', params.type); if (params.public_only !== undefined) queryParams.append('public_only', String(params.public_only)); if (params.page) queryParams.append('page', String(params.page)); if (params.page_size) queryParams.append('page_size', String(params.page_size)); const response = await fetch(`${API_BASE_URL}/texture?${queryParams.toString()}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); const result: RawApiResponse> = await response.json(); if (result.code === 200 && result.data) { return { ...result, data: normalizePaginatedData(result.data) }; } return result as ApiResponse>; } // 获取材质详情 export async function getTexture(id: number): Promise> { const response = await fetch(`${API_BASE_URL}/texture/${id}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); return response.json(); } // 切换收藏状态 export async function toggleFavorite(id: number): Promise> { const response = await fetch(`${API_BASE_URL}/texture/${id}/favorite`, { method: 'POST', headers: getAuthHeaders(), }); return response.json(); } // 获取用户上传的材质列表 export async function getMyTextures(params: { page?: number; page_size?: number; }): Promise>> { const queryParams = new URLSearchParams(); if (params.page) queryParams.append('page', String(params.page)); if (params.page_size) queryParams.append('page_size', String(params.page_size)); const response = await fetch(`${API_BASE_URL}/texture/my?${queryParams.toString()}`, { method: 'GET', headers: getAuthHeaders(), }); const result: RawApiResponse> = await response.json(); if (result.code === 200 && result.data) { return { ...result, data: normalizePaginatedData(result.data) }; } return result as ApiResponse>; } // 获取用户收藏的材质列表 export async function getFavoriteTextures(params: { page?: number; page_size?: number; }): Promise>> { const queryParams = new URLSearchParams(); if (params.page) queryParams.append('page', String(params.page)); if (params.page_size) queryParams.append('page_size', String(params.page_size)); const response = await fetch(`${API_BASE_URL}/texture/favorites?${queryParams.toString()}`, { method: 'GET', headers: getAuthHeaders(), }); const result: RawApiResponse> = await response.json(); if (result.code === 200 && result.data) { return { ...result, data: normalizePaginatedData(result.data) }; } return result as ApiResponse>; } // 获取用户档案列表 export async function getProfiles(): Promise> { const response = await fetch(`${API_BASE_URL}/profile`, { method: 'GET', headers: getAuthHeaders(), }); return response.json(); } // 创建档案 export async function createProfile(name: string): Promise> { const response = await fetch(`${API_BASE_URL}/profile`, { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ name }), }); return response.json(); } // 更新档案 // skin_id / cape_id: 显式传 null 表示移除当前皮肤的关联;传 number 表示设置;不传表示不修改。 export async function updateProfile(uuid: string, data: { name?: string; skin_id?: number | null; cape_id?: number | null; }): Promise> { const response = await fetch(`${API_BASE_URL}/profile/${uuid}`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data), }); return response.json(); } // 删除档案 export async function deleteProfile(uuid: string): Promise> { const response = await fetch(`${API_BASE_URL}/profile/${uuid}`, { method: 'DELETE', headers: getAuthHeaders(), }); return response.json(); } // 获取用户信息 export async function getUserProfile(): Promise> { const response = await fetch(`${API_BASE_URL}/user/profile`, { method: 'GET', headers: getAuthHeaders(), }); return response.json(); } // 更新用户信息 export async function updateUserProfile(data: { avatar?: string; old_password?: string; new_password?: string; }): Promise> { const response = await fetch(`${API_BASE_URL}/user/profile`, { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify(data), }); return response.json(); } // 直接上传皮肤文件 export async function uploadTexture(file: File, data: { name: string; description?: string; type?: 'SKIN' | 'CAPE'; is_public?: boolean; is_slim?: boolean; }): Promise> { const formData = new FormData(); formData.append('file', file); formData.append('name', data.name); if (data.description) formData.append('description', data.description); if (data.type) formData.append('type', data.type); if (data.is_public !== undefined) formData.append('is_public', String(data.is_public)); if (data.is_slim !== undefined) formData.append('is_slim', String(data.is_slim)); const response = await fetch(`${API_BASE_URL}/texture/upload`, { method: 'POST', headers: { ...(typeof window !== 'undefined' ? { Authorization: `Bearer ${localStorage.getItem('authToken')}` } : {}), }, body: formData, }); return response.json(); } // 直接上传头像文件到后端(multipart/form-data) // 后端接口 POST /user/avatar/upload,由后端负责写入对象存储并更新用户头像 export async function uploadAvatar(file: File): Promise> { 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: { ...(token && { Authorization: `Bearer ${token}` }), }, body: formData, }); return response.json(); } // 更新头像URL(用于外部URL场景,区别于文件直传) export async function updateAvatarUrl(avatarUrl: string): Promise> { const response = await fetch(`${API_BASE_URL}/user/avatar?avatar_url=${encodeURIComponent(avatarUrl)}`, { method: 'PUT', headers: getAuthHeaders(), }); return response.json(); } // 重置Yggdrasil密码 export async function resetYggdrasilPassword(): Promise> { const response = await fetch(`${API_BASE_URL}/user/yggdrasil-password/reset`, { method: 'POST', headers: getAuthHeaders(), }); return response.json(); } // 删除材质 export async function deleteTexture(id: number): Promise> { const response = await fetch(`${API_BASE_URL}/texture/${id}`, { method: 'DELETE', headers: getAuthHeaders(), }); return response.json(); } // 更新材质信息(名称、描述、公开性) // 对应后端 PUT /texture/{id}:is_public 为布尔值时会更新该字段,会影响公开/隐藏状态 export async function updateTexture(id: number, data: { name?: string; description?: string; is_public?: boolean; }): Promise> { 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> { 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> { 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> { 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(); }