api.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import type { AdminStats, Course, CourseStatus, User } from './types'
  2. const token = () => localStorage.getItem('xinghen_token')
  3. async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
  4. const headers = new Headers(init.headers)
  5. if (token()) headers.set('Authorization', `Bearer ${token()}`)
  6. const response = await fetch(`/api${path}`, { ...init, headers })
  7. if (!response.ok) {
  8. const body = await response.json().catch(() => ({ message: '请求失败,请稍后重试' }))
  9. if (response.status === 401) {
  10. localStorage.removeItem('xinghen_token')
  11. window.dispatchEvent(new Event('auth-expired'))
  12. }
  13. throw new Error(body.message || '请求失败,请稍后重试')
  14. }
  15. return response.json() as Promise<T>
  16. }
  17. export const api = {
  18. // 用户鉴权与信息
  19. register: (phone: string, password: string) =>
  20. request<{ token: string; user: User }>('/auth/register', {
  21. method: 'POST',
  22. headers: { 'Content-Type': 'application/json' },
  23. body: JSON.stringify({ phone, password })
  24. }),
  25. login: (phone: string, password: string) =>
  26. request<{ token: string; user: User }>('/auth/login', {
  27. method: 'POST',
  28. headers: { 'Content-Type': 'application/json' },
  29. body: JSON.stringify({ phone, password })
  30. }),
  31. logout: () => request<{ ok: boolean }>('/auth/logout', { method: 'POST' }),
  32. me: () => request<{ user: User }>('/me'),
  33. updateMe: (body: Pick<User, 'organization' | 'wechat' | 'contactName' | 'bio'>) =>
  34. request<{ user: User }>('/me', {
  35. method: 'PATCH',
  36. headers: { 'Content-Type': 'application/json' },
  37. body: JSON.stringify(body)
  38. }),
  39. changePassword: (currentPassword: string, newPassword: string) =>
  40. request<{ ok: boolean }>('/me/password', {
  41. method: 'POST',
  42. headers: { 'Content-Type': 'application/json' },
  43. body: JSON.stringify({ currentPassword, newPassword })
  44. }),
  45. // 普通用户课程操作
  46. listCourses: () => request<{ courses: Course[] }>('/courses'),
  47. createCourse: (body: Pick<Course, 'name' | 'category' | 'audience' | 'description'>) =>
  48. request<{ course: Course }>('/courses', {
  49. method: 'POST',
  50. headers: { 'Content-Type': 'application/json' },
  51. body: JSON.stringify(body)
  52. }),
  53. getCourse: (id: string) => request<{ course: Course }>(`/courses/${id}`),
  54. uploadAssets: (id: string, files: File[]) => {
  55. const form = new FormData()
  56. files.forEach((f) => form.append('assets', f))
  57. return request<{ course: Course }>(`/courses/${id}/assets`, { method: 'POST', body: form })
  58. },
  59. deleteAsset: (id: string, assetId: string) =>
  60. request<{ course: Course }>(`/courses/${id}/assets/${assetId}`, { method: 'DELETE' }),
  61. addInstructor: (
  62. id: string,
  63. data: { name: string; organization: string; introduction: string; image?: File }
  64. ) => {
  65. const f = new FormData()
  66. f.append('name', data.name)
  67. f.append('organization', data.organization)
  68. f.append('introduction', data.introduction)
  69. if (data.image) f.append('image', data.image)
  70. return request<{ course: Course }>(`/courses/${id}/instructors`, { method: 'POST', body: f })
  71. },
  72. deleteInstructor: (id: string, instructorId: string) =>
  73. request<{ course: Course }>(`/courses/${id}/instructors/${instructorId}`, { method: 'DELETE' }),
  74. submitCourse: (id: string) => request<{ course: Course }>(`/courses/${id}/submit`, { method: 'POST' }),
  75. // 管理员后台专区 API
  76. admin: {
  77. login: (phone: string, password: string) =>
  78. request<{ token: string; user: User }>('/admin/auth/login', {
  79. method: 'POST',
  80. headers: { 'Content-Type': 'application/json' },
  81. body: JSON.stringify({ phone, password })
  82. }),
  83. getStats: () => request<AdminStats>('/admin/stats'),
  84. listCourses: (params?: { status?: string; keyword?: string }) => {
  85. const q = new URLSearchParams()
  86. if (params?.status) q.set('status', params.status)
  87. if (params?.keyword) q.set('keyword', params.keyword)
  88. const qs = q.toString() ? `?${q.toString()}` : ''
  89. return request<{ courses: Course[]; total: number }>(`/admin/courses${qs}`)
  90. },
  91. getCourse: (id: string) => request<{ course: Course }>(`/admin/courses/${id}`),
  92. updateStatus: (id: string, status: CourseStatus) =>
  93. request<{ course: Course }>(`/admin/courses/${id}/status`, {
  94. method: 'POST',
  95. headers: { 'Content-Type': 'application/json' },
  96. body: JSON.stringify({ status })
  97. }),
  98. uploadDeliverables: (id: string, files: File[]) => {
  99. const form = new FormData()
  100. files.forEach((f) => form.append('deliverables', f))
  101. return request<{ course: Course }>(`/admin/courses/${id}/deliverables`, { method: 'POST', body: form })
  102. },
  103. deleteDeliverable: (id: string, delivId: string) =>
  104. request<{ course: Course }>(`/admin/courses/${id}/deliverables/${delivId}`, { method: 'DELETE' }),
  105. completeCourse: (id: string, productionNotes: string) =>
  106. request<{ course: Course }>(`/admin/courses/${id}/complete`, {
  107. method: 'POST',
  108. headers: { 'Content-Type': 'application/json' },
  109. body: JSON.stringify({ productionNotes })
  110. }),
  111. listUsers: (keyword?: string) => {
  112. const qs = keyword ? `?keyword=${encodeURIComponent(keyword)}` : ''
  113. return request<{ users: User[] }>(`/admin/users${qs}`)
  114. },
  115. updateUserRole: (id: string, role: 'USER' | 'ADMIN') =>
  116. request<{ user: User }>(`/admin/users/${id}/role`, {
  117. method: 'PATCH',
  118. headers: { 'Content-Type': 'application/json' },
  119. body: JSON.stringify({ role })
  120. })
  121. }
  122. }