import type { User, UserPublic, UserActivityStats, Board, PostItem, Comment, RecentComment, ForumStats, TagCount, AdminDashboard, AdminSettings, ForumLimits, ForumLimitsPublic, PostDetailResponse, PostRevision, CommentRevision, MailConfig, OIDCConfig, OAuthClient, OAuthClientInput, GiteaProject, GiteaSyncConfig, StorageConfig, MediaListResult, SiteBranding, RegisterConfig, PrivateMessage, MessageConversation, PostReport, ReportReason, ReportStatus, BadgeDef, PointLedger, CheckInStatus, LotteryStatus } from './types'; const BASE = ''; async function request(url: string, opts: RequestInit = {}): Promise { const res = await fetch(BASE + url, { credentials: 'same-origin', ...opts, headers: { ...(opts.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }), ...opts.headers, }, }); let data: Record; try { data = await res.json(); } catch { throw new Error('服务器响应异常'); } if (!res.ok) throw new Error((data.error as string) || '请求失败'); return data as T; } export const api = { me: () => request<{ user: User | null }>('/api/me'), stats: () => request('/api/stats'), forumLimits: () => request('/api/forum-limits'), siteBranding: () => request('/api/site-branding'), boards: () => request<{ boards: Board[] }>('/api/boards'), projects: (params?: { page?: number; limit?: number }) => { const q = new URLSearchParams(); if (params?.page) q.set('page', String(params.page)); if (params?.limit) q.set('limit', String(params.limit)); const qs = q.toString(); return request<{ projects: GiteaProject[]; total: number; page: number; total_pages: number }>( `/api/projects${qs ? `?${qs}` : ''}`, ); }, posts: (params: Record) => { const q = new URLSearchParams(params as Record).toString(); return request<{ posts: PostItem[]; total: number; page: number; has_more: boolean }>(`/api/posts?${q}`); }, hotPosts: () => request<{ posts: PostItem[] }>('/api/posts/hot'), tags: (limit = 40) => request<{ tags: TagCount[] }>(`/api/tags?limit=${limit}`), post: (id: number, opts?: { skipView?: boolean }) => { const q = opts?.skipView ? '?skip_view=1' : ''; return request(`/api/posts/${id}${q}`); }, comments: (id: number, myIds?: number[]) => { const q = myIds?.length ? `?my_ids=${myIds.join(',')}` : ''; return request<{ comments: Comment[]; total: number }>(`/api/posts/${id}/comments${q}`); }, recentComments: () => request<{ comments: RecentComment[] }>('/api/comments/recent'), favorites: () => request<{ favorites: unknown[]; total: number }>('/api/favorites'), createBoard: (body: { name: string; description: string; sort_order: number; icon?: string; color_index?: number }) => request<{ board: Board }>('/api/admin/boards', { method: 'POST', body: JSON.stringify(body) }), updateBoard: (id: number, body: { name: string; description: string; sort_order: number; icon?: string; color_index?: number }) => request<{ board: Board }>(`/api/admin/boards/${id}`, { method: 'PUT', body: JSON.stringify(body) }), deleteBoard: (id: number) => request(`/api/admin/boards/${id}`, { method: 'DELETE' }), // 管理后台 API adminDashboard: () => request('/api/admin/dashboard'), adminSettings: () => request('/api/admin/settings'), adminPosts: (params: { page?: number; keyword?: string; status?: string }) => { const q = new URLSearchParams(); if (params.page) q.set('page', String(params.page)); if (params.keyword) q.set('keyword', params.keyword); if (params.status) q.set('status', params.status); const qs = q.toString(); return request<{ posts: PostItem[]; total: number; page: number; total_pages: number; pending_count?: number; status?: string; }>(`/api/admin/posts${qs ? `?${qs}` : ''}`); }, adminApprovePost: (id: number) => request<{ message: string; status: string }>(`/api/admin/posts/${id}/approve`, { method: 'POST' }), adminPinPost: (id: number, pinned: boolean) => request<{ message: string; pinned: boolean }>(`/api/admin/posts/${id}/pin`, { method: 'POST', body: JSON.stringify({ pinned }), }), adminFeaturePost: (id: number, featured: boolean) => request<{ message: string; featured: boolean }>(`/api/admin/posts/${id}/feature`, { method: 'POST', body: JSON.stringify({ featured }), }), adminLockPost: (id: number, locked: boolean) => request<{ message: string; edit_locked: boolean }>(`/api/admin/posts/${id}/lock`, { method: 'POST', body: JSON.stringify({ locked }), }), adminRejectPost: (id: number, reason: string) => request<{ message: string; notified: boolean }>(`/api/admin/posts/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason }), }), adminReports: (params?: { page?: number; status?: ReportStatus | 'all' | string }) => { const q = new URLSearchParams(); if (params?.page) q.set('page', String(params.page)); if (params?.status) q.set('status', params.status); const qs = q.toString(); return request<{ reports: PostReport[]; total: number; page: number; pending_count: number; status: string; }>(`/api/admin/reports${qs ? `?${qs}` : ''}`); }, adminHandleReport: (id: number, body: { action: 'dismiss' | 'resolve' | 'reject_post' | 'reject_comment'; handle_note?: string; reject_reason?: string; }) => request<{ message: string; report: PostReport }>(`/api/admin/reports/${id}/handle`, { method: 'POST', body: JSON.stringify(body), }), adminUpdateForumSettings: (body: ForumLimits) => request<{ message: string; limits: ForumLimits }>('/api/admin/settings/forum', { method: 'PUT', body: JSON.stringify(body), }), adminUpdateMailSettings: (body: MailConfig) => request<{ message: string; mail: MailConfig }>('/api/admin/settings/mail', { method: 'PUT', body: JSON.stringify(body), }), adminUpdateOIDCSettings: (body: OIDCConfig) => request<{ message: string; oidc: OIDCConfig }>('/api/admin/settings/oidc', { method: 'PUT', body: JSON.stringify(body), }), adminUpdateGiteaSettings: (body: GiteaSyncConfig) => request<{ message: string; gitea: GiteaSyncConfig }>('/api/admin/settings/gitea', { method: 'PUT', body: JSON.stringify(body), }), adminSyncGitea: () => request<{ message: string; count: number; gitea: GiteaSyncConfig }>('/api/admin/settings/gitea/sync', { method: 'POST', }), adminUpdateStorageSettings: (body: StorageConfig) => request<{ message: string; storage: StorageConfig }>('/api/admin/settings/storage', { method: 'PUT', body: JSON.stringify(body), }), adminUpdateBranding: (body: SiteBranding) => request<{ message: string; branding: SiteBranding }>('/api/admin/settings/branding', { method: 'PUT', body: JSON.stringify(body), }), adminUploadBrandingAsset: (kind: 'logo' | 'favicon' | 'og_image', file: File) => { const fd = new FormData(); fd.append('kind', kind); fd.append('file', file); return request<{ message: string; url: string; branding: SiteBranding }>( '/api/admin/settings/branding/upload', { method: 'POST', body: fd, headers: {} }, ); }, adminClearBrandingAsset: (kind: 'logo' | 'favicon' | 'og_image') => request<{ message: string; branding: SiteBranding }>('/api/admin/settings/branding/clear', { method: 'POST', body: JSON.stringify({ kind }), }), adminListOAuthClients: () => request<{ clients: OAuthClient[] }>('/api/admin/oauth/clients'), adminCreateOAuthClient: (body: OAuthClientInput) => request<{ message: string; client: OAuthClient; oidc: OIDCConfig }>('/api/admin/oauth/clients', { method: 'POST', body: JSON.stringify(body), }), adminUpdateOAuthClient: (id: number, body: OAuthClientInput) => request<{ message: string; client: OAuthClient; oidc: OIDCConfig }>(`/api/admin/oauth/clients/${id}`, { method: 'PUT', body: JSON.stringify(body), }), adminDeleteOAuthClient: (id: number) => request<{ message: string; oidc: OIDCConfig }>(`/api/admin/oauth/clients/${id}`, { method: 'DELETE', }), adminTestMail: (to: string) => request<{ message: string }>('/api/admin/settings/mail/test', { method: 'POST', body: JSON.stringify({ to }), }), adminUpdateFilterWords: (content: string) => request<{ message: string; word_count: number }>('/api/admin/settings/filter-words', { method: 'PUT', body: JSON.stringify({ content }), }), postRevisions: (id: number) => request<{ revisions: PostRevision[] }>(`/api/posts/${id}/revisions`), postRevision: (id: number, revId: number) => request<{ revision: PostRevision }>(`/api/posts/${id}/revisions/${revId}`), adminDeletePost: (id: number) => request(`/api/admin/posts/${id}`, { method: 'DELETE' }), adminTrashPosts: (params: { page?: number; keyword?: string }) => { const q = new URLSearchParams(); if (params.page) q.set('page', String(params.page)); if (params.keyword) q.set('keyword', params.keyword); const qs = q.toString(); return request<{ posts: (PostItem & { deleted_at: string })[]; total: number; page: number; total_pages: number }>( `/api/admin/posts/trash${qs ? `?${qs}` : ''}`, ); }, adminRestorePost: (id: number) => request<{ message: string }>(`/api/admin/posts/${id}/restore`, { method: 'POST' }), adminPurgePost: (id: number) => request<{ message: string }>(`/api/admin/posts/${id}/purge`, { method: 'DELETE' }), adminComments: (params?: { page?: number; status?: string }) => { const q = new URLSearchParams(); if (params?.page) q.set('page', String(params.page)); if (params?.status) q.set('status', params.status); const qs = q.toString(); return request<{ comments: Comment[]; total: number; page: number; total_pages: number; pending_count?: number; }>(`/api/admin/comments${qs ? `?${qs}` : ''}`); }, adminApproveComment: (id: number) => request<{ message: string; status: string }>(`/api/admin/comments/${id}/approve`, { method: 'POST' }), adminRejectComment: (id: number, reason?: string) => request<{ message: string; status: string }>(`/api/admin/comments/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason: reason || '' }), }), adminDeleteComment: (id: number) => request(`/api/admin/comments/${id}`, { method: 'DELETE' }), adminTrashComments: (params?: { page?: number; keyword?: string }) => { const q = new URLSearchParams(); if (params?.page) q.set('page', String(params.page)); if (params?.keyword) q.set('keyword', params.keyword); const qs = q.toString(); return request<{ comments: (Comment & { deleted_at: string })[]; total: number; page: number; total_pages: number; }>(`/api/admin/comments/trash${qs ? `?${qs}` : ''}`); }, adminRestoreComment: (id: number) => request<{ message: string }>(`/api/admin/comments/${id}/restore`, { method: 'POST' }), adminPurgeComment: (id: number) => request<{ message: string }>(`/api/admin/comments/${id}/purge`, { method: 'DELETE' }), adminCommentRevisions: (id: number) => request<{ revisions: CommentRevision[] }>(`/api/admin/comments/${id}/revisions`), adminUsers: (page = 1, opts?: { keyword?: string; filter?: string }) => { const q = new URLSearchParams({ page: String(page) }); if (opts?.keyword?.trim()) q.set('keyword', opts.keyword.trim()); if (opts?.filter && opts.filter !== 'all') q.set('filter', opts.filter); return request<{ users: User[]; total: number; page: number; total_pages: number }>( `/api/admin/users?${q}`, ); }, adminBanUser: (id: number, banned: boolean) => request<{ message: string; banned: boolean }>(`/api/admin/users/${id}/ban`, { method: 'POST', body: JSON.stringify({ banned }), }), adminVerifyUser: (id: number, verified: boolean) => request<{ message: string; verified: boolean }>(`/api/admin/users/${id}/verify`, { method: 'POST', body: JSON.stringify({ verified }), }), adminSetUserLevel: (id: number, level: number) => request<{ message: string; level: number; exp: number }>(`/api/admin/users/${id}/level`, { method: 'POST', body: JSON.stringify({ level }), }), adminAdjustPoints: (id: number, delta: number, note?: string) => request<{ message: string; points: number }>(`/api/admin/users/${id}/points`, { method: 'POST', body: JSON.stringify({ delta, note: note || '' }), }), adminListBadges: () => request<{ badges: BadgeDef[] }>('/api/admin/badges'), adminUpsertBadge: (badge: Partial) => request<{ message: string; badge: BadgeDef }>('/api/admin/badges', { method: 'POST', body: JSON.stringify(badge), }), adminAwardBadge: (userId: number, badgeId: number, revoke = false) => request<{ message: string }>(`/api/admin/users/${userId}/badges`, { method: 'POST', body: JSON.stringify({ badge_id: badgeId, revoke }), }), mePoints: (page = 1) => request<{ points: number; creator_income_total: number; ledger: PointLedger[]; total: number; page: number; total_pages: number; check_in: CheckInStatus; lottery: LotteryStatus; }>(`/api/me/points?page=${page}`), checkIn: () => request<{ message: string; check_in: CheckInStatus; points: number }>('/api/me/check-in', { method: 'POST' }), lotteryStatus: () => request<{ lottery: LotteryStatus }>('/api/me/lottery'), lotteryDraw: () => request<{ message: string; lottery: LotteryStatus; points: number }>('/api/me/lottery', { method: 'POST' }), unlockPostBlock: (postId: number, blockKey: string) => request<{ message: string; unlock: { block_key: string; cost: number; points_balance: number; inner_html: string } }>( `/api/posts/${postId}/unlock`, { method: 'POST', body: JSON.stringify({ block_key: blockKey }) }, ), adminMedia: (params?: { category?: string; page?: number; size?: number; q?: string }) => { const sp = new URLSearchParams(); if (params?.category) sp.set('category', params.category); if (params?.page) sp.set('page', String(params.page)); if (params?.size) sp.set('size', String(params.size)); if (params?.q) sp.set('q', params.q); const qs = sp.toString(); return request(`/api/admin/media${qs ? `?${qs}` : ''}`); }, adminDeleteMedia: (urls: string[]) => request<{ message: string; deleted: number }>('/api/admin/media/delete', { method: 'POST', body: JSON.stringify({ urls }), }), adminBackup: () => request<{ message: string; filename: string; download: string }>('/api/admin/backup', { method: 'POST' }), profileStats: () => request<{ stats: UserActivityStats }>('/api/profile/stats'), userProfile: (id: number) => request<{ user: UserPublic; stats: UserActivityStats }>(`/api/users/${id}`), updateNickname: (nickname: string) => { const fd = new FormData(); fd.append('nickname', nickname); return request('/api/profile/nickname', { method: 'POST', body: fd, headers: {} }); }, updateSignature: (signature: string) => { const fd = new FormData(); fd.append('signature', signature); return request<{ message: string; user: User }>('/api/profile/signature', { method: 'POST', body: fd, headers: {}, }); }, updatePassword: (oldPassword: string, newPassword: string) => { const fd = new FormData(); fd.append('old_password', oldPassword); fd.append('new_password', newPassword); return request('/api/profile/password', { method: 'POST', body: fd, headers: {} }); }, uploadAvatar: (file: File) => { const fd = new FormData(); fd.append('avatar', file); return request<{ avatar: string }>('/api/profile/avatar', { method: 'POST', body: fd, headers: {} }); }, uploadPostImage: (file: File) => { const fd = new FormData(); fd.append('image', file); return request<{ url: string }>('/api/uploads/image', { method: 'POST', body: fd, headers: {} }); }, createPost: (data: { board_id: string; title: string; content: string; tags?: string; post_type?: string }) => { const fd = new FormData(); fd.append('board_id', data.board_id); fd.append('title', data.title); fd.append('content', data.content); fd.append('tags', data.tags || ''); fd.append('post_type', data.post_type || 'normal'); return request<{ post_id: number; message?: string; status?: string }>('/api/posts', { method: 'POST', body: fd, headers: {} }); }, updatePost: (id: number, data: { title: string; content: string; tags?: string; board_id?: string | number; post_type?: string }) => { const fd = new FormData(); fd.append('title', data.title); fd.append('content', data.content); fd.append('tags', data.tags || ''); if (data.board_id != null && data.board_id !== '') { fd.append('board_id', String(data.board_id)); } if (data.post_type) { fd.append('post_type', data.post_type); } return request<{ message: string }>(`/api/posts/${id}`, { method: 'PUT', body: fd, headers: {} }); }, setQuestionResolved: (id: number, resolved: boolean) => { const fd = new FormData(); fd.append('resolved', resolved ? '1' : '0'); return request<{ message: string; question_resolved: boolean }>(`/api/posts/${id}/resolve`, { method: 'POST', body: fd, headers: {}, }); }, deletePost: (id: number) => request<{ message: string }>(`/api/posts/${id}`, { method: 'DELETE' }), login: (username: string, password: string) => { const fd = new FormData(); fd.append('username', username); fd.append('password', password); return request('/api/login', { method: 'POST', body: fd, headers: {} }); }, register: (data: { username: string; password: string; nickname: string; email: string; emailCode?: string; }) => { const fd = new FormData(); fd.append('username', data.username); fd.append('password', data.password); fd.append('nickname', data.nickname); fd.append('email', data.email); if (data.emailCode) fd.append('email_code', data.emailCode); return request('/api/register', { method: 'POST', body: fd, headers: {} }); }, registerConfig: () => request('/api/register/config'), sendRegisterEmailCode: (email: string) => request<{ message: string }>('/api/register/email-code', { method: 'POST', body: JSON.stringify({ email }), }), captcha: () => request<{ id: string; image: string }>('/api/captcha'), logout: () => request('/api/logout', { method: 'POST' }), like: (id: number) => request<{ liked: boolean; like_count: number }>(`/api/posts/${id}/like`, { method: 'POST' }), likeComment: (id: number) => request<{ liked: boolean; like_count: number }>(`/api/comments/${id}/like`, { method: 'POST' }), favorite: (id: number) => request<{ favorited: boolean }>(`/api/posts/${id}/favorite`, { method: 'POST' }), reportPost: (id: number, body: { reason: ReportReason; detail?: string }) => request<{ message: string; report: PostReport }>(`/api/posts/${id}/report`, { method: 'POST', body: JSON.stringify(body), }), reportComment: (id: number, body: { reason: ReportReason; detail?: string }) => request<{ message: string; report: PostReport }>(`/api/comments/${id}/report`, { method: 'POST', body: JSON.stringify(body), }), messageConversations: (params?: { page?: number; size?: number }) => { const q = new URLSearchParams(); if (params?.page) q.set('page', String(params.page)); if (params?.size) q.set('size', String(params.size)); const qs = q.toString(); return request<{ conversations: MessageConversation[]; total: number; page: number }>( `/api/messages/conversations${qs ? `?${qs}` : ''}`, ); }, conversationMessages: (peerId: number, params?: { size?: number; before?: number }) => { const q = new URLSearchParams(); if (params?.size) q.set('size', String(params.size)); if (params?.before) q.set('before', String(params.before)); const qs = q.toString(); return request<{ messages: PrivateMessage[]; total: number; peer_user_id: number; peer_user?: User; is_system: boolean; }>(`/api/messages/conversations/${peerId}${qs ? `?${qs}` : ''}`); }, markConversationRead: (peerId: number) => request<{ message: string }>(`/api/messages/conversations/${peerId}/read`, { method: 'POST' }), messageUnreadCount: () => request<{ count: number }>('/api/messages/unread-count'), sendMessage: (body: { to_user_id: number; subject?: string; content: string }) => request<{ message: PrivateMessage }>('/api/messages', { method: 'POST', body: JSON.stringify(body), }), markAllMessagesRead: () => request<{ message: string }>('/api/messages/read-all', { method: 'POST' }), addComment: (postId: number, data: { content: string; replyTo?: number; isPrivate?: boolean; }) => { const fd = new FormData(); fd.append('content', data.content); if (data.replyTo) fd.append('reply_to', String(data.replyTo)); if (data.isPrivate) fd.append('is_private', '1'); return request<{ message: string; floor: number; id: number; status?: string }>(`/api/posts/${postId}/comments`, { method: 'POST', body: fd, headers: {} }); }, updateComment: (id: number, content: string) => { const fd = new FormData(); fd.append('content', content); return request<{ message: string; content: string; status?: string }>(`/api/comments/${id}`, { method: 'PUT', body: fd, headers: {} }); }, deleteComment: (id: number) => request<{ message: string }>(`/api/comments/${id}`, { method: 'DELETE' }), };