| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- import type { AdminStats, Course, CourseStatus, CourseTemplate, DigitalInstructor, Lead, LeadStatus, Organization, PointsTransaction, User } from './types'
- const token = () => localStorage.getItem('xinghen_token')
- async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
- const headers = new Headers(init.headers)
- if (token()) headers.set('Authorization', `Bearer ${token()}`)
- if (!headers.has('X-Workspace-Id')) headers.set('X-Workspace-Id', localStorage.getItem('xinghen_workspace') || 'personal')
- const response = await fetch(`/api${path}`, { ...init, headers })
- if (!response.ok) {
- const body = await response.json().catch(() => ({ message: '请求失败,请稍后重试' }))
- if (response.status === 401) {
- localStorage.removeItem('xinghen_token')
- window.dispatchEvent(new Event('auth-expired'))
- }
- throw new Error(body.message || '请求失败,请稍后重试')
- }
- const result = await response.json() as T
- if (init.method && init.method !== 'GET') window.dispatchEvent(new Event('workspace-updated'))
- return result
- }
- export async function downloadPrivateFile(url: string, filename: string) {
- const response = await fetch(url.replace('/uploads/', '/api/files/'), {
- headers: { Authorization: `Bearer ${token()}` }
- })
- if (!response.ok) throw new Error('文件下载失败')
- const objectUrl = URL.createObjectURL(await response.blob())
- const link = document.createElement('a')
- link.href = objectUrl
- link.download = filename
- link.click()
- setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000)
- }
- export async function privateImageUrl(url: string) {
- const response = await fetch(url.replace('/uploads/', '/api/files/'), {
- headers: { Authorization: `Bearer ${token()}` }
- })
- if (!response.ok) throw new Error('图片不可访问')
- return URL.createObjectURL(await response.blob())
- }
- export async function fetchPrivateAsset(url: string) {
- const response = await fetch(url.replace('/uploads/', '/api/files/'), {
- headers: { Authorization: `Bearer ${token()}` }
- })
- if (!response.ok) throw new Error('素材读取失败,请确认登录状态和课程权限')
- return response.blob()
- }
- export async function fetchOfficePreview(courseId: string, assetId: string) {
- const response = await fetch(`/api/courses/${encodeURIComponent(courseId)}/assets/${encodeURIComponent(assetId)}/preview`, {
- headers: {
- Authorization: `Bearer ${token()}`,
- 'X-Workspace-Id': localStorage.getItem('xinghen_workspace') || 'personal'
- }
- })
- if (!response.ok) {
- const body = await response.json().catch(() => null)
- throw new Error(body?.message || '预览生成失败')
- }
- return response.blob()
- }
- export const api = {
- digitalInstructors: {
- list: (workspaceId?: string) => request<{ instructors: DigitalInstructor[] }>('/digital-instructors', workspaceId ? { headers: { 'X-Workspace-Id': workspaceId } } : {}),
- get: (id: string, workspaceId?: string) => request<{ instructor: DigitalInstructor }>(`/digital-instructors/${id}`, workspaceId ? { headers: { 'X-Workspace-Id': workspaceId } } : {}),
- usage: (id: string, workspaceId?: string) => request<{ courses: { id: string; name: string; status: string }[] }>(`/digital-instructors/${id}/usage`, workspaceId ? { headers: { 'X-Workspace-Id': workspaceId } } : {}),
- create: (data: { name: string; note: string; images: File[]; audio?: File | null; coverNewIndex?: number }) => {
- const form = new FormData()
- form.append('name', data.name)
- form.append('note', data.note)
- if (data.coverNewIndex !== undefined) form.append('coverNewIndex', String(data.coverNewIndex))
- data.images.forEach(file => form.append('images', file))
- if (data.audio) form.append('audio', data.audio)
- return request<{ instructor: DigitalInstructor }>('/digital-instructors', { method: 'POST', body: form })
- },
- update: (id: string, data: Pick<DigitalInstructor, 'name' | 'note'>, workspaceId?: string) => request<{ instructor: DigitalInstructor }>(`/digital-instructors/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...(workspaceId ? { 'X-Workspace-Id': workspaceId } : {}) }, body: JSON.stringify(data) }),
- manage: (id: string, data: { name: string; note: string; ownerId?: string; images: File[]; audio: File | null; removeImageIds: string[]; coverImageId?: string; coverNewIndex?: number }, workspaceId?: string) => {
- const form = new FormData()
- form.append('name', data.name); form.append('note', data.note)
- if (data.ownerId) form.append('ownerId', data.ownerId)
- form.append('removeImageIds', JSON.stringify(data.removeImageIds))
- if (data.coverImageId) form.append('coverImageId', data.coverImageId)
- if (data.coverNewIndex !== undefined) form.append('coverNewIndex', String(data.coverNewIndex))
- data.images.forEach(file => form.append('images', file))
- if (data.audio) form.append('audio', data.audio)
- return request<{ instructor: DigitalInstructor }>(`/digital-instructors/${id}/manage`, { method: 'POST', body: form, ...(workspaceId ? { headers: { 'X-Workspace-Id': workspaceId } } : {}) })
- },
- owner: (id: string, ownerId: string, workspaceId?: string) => request<{ instructor: DigitalInstructor }>(`/digital-instructors/${id}/owner`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...(workspaceId ? { 'X-Workspace-Id': workspaceId } : {}) }, body: JSON.stringify({ ownerId }) }),
- copy: (id: string, organizationId: string) => request<{ instructor: DigitalInstructor }>(`/digital-instructors/${id}/copy-to-organization`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ organizationId }) }),
- duplicate: (id: string, workspaceId?: string) => request<{ instructor: DigitalInstructor }>(`/digital-instructors/${id}/duplicate`, { method: 'POST', ...(workspaceId ? { headers: { 'X-Workspace-Id': workspaceId } } : {}) }),
- delete: (id: string, workspaceId?: string) => request<{ ok: true }>(`/digital-instructors/${id}`, { method: 'DELETE', ...(workspaceId ? { headers: { 'X-Workspace-Id': workspaceId } } : {}) })
- },
- organizations: {
- mine: () => request<{ organizations: Organization[] }>('/me/organizations'),
- publicInfo: (slug: string) => request<{ organization: { id: string; name: string; isActive: boolean } | null }>(`/organizations/${slug}`),
- content: (id: string) => request<any>(`/organizations/${id}/content`),
- apply: (slug: string) => request<{ ok: boolean }>(`/organizations/${slug}/apply`, { method: 'POST' }),
- join: (inviteToken: string) => request<{ organizationId: string }>(`/organizations/join/${inviteToken}`, { method: 'POST' }),
- manage: (id: string) => request<any>(`/organizations/${id}/manage`),
- review: (id: string, applicationId: string, approve: boolean) => request<{ ok: boolean }>(`/organizations/${id}/applications/${applicationId}/review`, {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ approve })
- }),
- createUser: (id: string, phone: string, contactName: string) => request<{ user: User; initialPassword: string }>(`/organizations/${id}/users`, {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone, contactName })
- }),
- updateMemberName: (id: string, userId: string, contactName: string) => request<{ user: User }>(`/organizations/${id}/members/${userId}`, {
- method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contactName })
- }),
- removeMember: (id: string, userId: string) => request<{ ok: boolean }>(`/organizations/${id}/members/${userId}`, {
- method: 'DELETE'
- }),
- rotateInvite: (id: string) => request<{ token: string }>(`/organizations/${id}/invite`, { method: 'POST' }),
- disableInvite: (id: string) => request<{ ok: boolean }>(`/organizations/${id}/invite`, { method: 'DELETE' }),
- points: async (id: string) => {
- const result = await request<{ balance: number; transactions: any[] }>(`/organizations/${id}/points`)
- return { balance: result.balance, transactions: result.transactions.map((row) => ({
- id: row.id, courseId: row.course_id, courseName: row.course_name || row.reason, points: row.amount, createdAt: row.created_at
- })) as PointsTransaction[] }
- }
- },
- // 用户鉴权与信息
- register: (phone: string, password: string, contactName: string, inviteToken?: string) =>
- request<{ token: string; user: User }>('/auth/register', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ phone, password, contactName, inviteToken })
- }),
- login: (phone: string, password: string) =>
- request<{ token: string; user: User }>('/auth/login', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ phone, password })
- }),
- logout: () => request<{ ok: boolean }>('/auth/logout', { method: 'POST' }),
- me: () => request<{ user: User }>('/me'),
- getPointsTransactions: () =>
- request<{ balance: number; transactions: PointsTransaction[] }>('/me/points-transactions'),
- updateMe: (body: Pick<User, 'wechat' | 'contactName' | 'bio'>) =>
- request<{ user: User }>('/me', {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- }),
- changePassword: (currentPassword: string, newPassword: string) =>
- request<{ ok: boolean }>('/me/password', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ currentPassword, newPassword })
- }),
- // 课程模板
- getTemplates: () => request<{ templates: CourseTemplate[] }>('/course-templates'),
- createFromTemplate: (templateId?: string) =>
- request<{ course: Course }>('/courses/template', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ templateId })
- }),
- // 普通用户课程操作
- listCourses: () => request<{ courses: Course[] }>('/courses'),
- createCourse: (body: Pick<Course, 'name' | 'description'> & { digitalInstructorId?: string | null }) =>
- request<{ course: Course }>('/courses', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- }),
- getCourse: (id: string) => request<{ course: Course }>(`/courses/${id}`),
- updateCourse: (id: string, body: Partial<Pick<Course, 'name' | 'description' | 'assetNotes'>> & { digitalInstructorId?: string | null }) =>
- request<{ course: Course }>(`/courses/${id}`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- }),
- deleteCourse: (id: string) => request<{ ok: boolean; message?: string }>(`/courses/${id}`, { method: 'DELETE' }),
- uploadAssets: (id: string, files: File[], episodeId?: string) => {
- const form = new FormData()
- files.forEach((f) => form.append('assets', f))
- if (episodeId) form.append('episodeId', episodeId)
- return request<{ course: Course; notesImported: number }>(`/courses/${id}/assets`, { method: 'POST', body: form })
- },
- deleteAsset: (id: string, assetId: string) =>
- request<{ course: Course }>(`/courses/${id}/assets/${assetId}`, { method: 'DELETE' }),
- // 课程分集操作
- createEpisode: (
- courseId: string,
- data: { title: string; episodeNumber?: number; summary?: string; lectureNotes?: string }
- ) =>
- request<{ course: Course }>(`/courses/${courseId}/episodes`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- }),
- batchCreateEpisodes: (
- courseId: string,
- data: { count?: number; episodes?: Array<{ title: string; episodeNumber?: number; summary?: string; lectureNotes?: string }> }
- ) =>
- request<{ course: Course }>(`/courses/${courseId}/episodes/batch`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- }),
- updateEpisode: (
- courseId: string,
- episodeId: string,
- data: { title?: string; episodeNumber?: number; summary?: string; lectureNotes?: string }
- ) =>
- request<{ course: Course }>(`/courses/${courseId}/episodes/${episodeId}`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- }),
- deleteEpisode: (courseId: string, episodeId: string) =>
- request<{ course: Course }>(`/courses/${courseId}/episodes/${episodeId}`, { method: 'DELETE' }),
- addInstructor: (
- id: string,
- data: { name: string; organization: string; introduction: string; images?: File[] }
- ) => {
- const f = new FormData()
- f.append('name', data.name)
- f.append('organization', data.organization)
- f.append('introduction', data.introduction)
- data.images?.forEach((image) => f.append('images', image))
- return request<{ course: Course }>(`/courses/${id}/instructors`, { method: 'POST', body: f })
- },
- deleteInstructor: (id: string, instructorId: string) =>
- request<{ course: Course }>(`/courses/${id}/instructors/${instructorId}`, { method: 'DELETE' }),
- submitCourse: (id: string) => request<{ course: Course }>(`/courses/${id}/submit`, { method: 'POST' }),
- cancelCourseSubmission: (id: string) =>
- request<{ course: Course }>(`/courses/${id}/cancel-submission`, { method: 'POST' }),
- // 管理员后台专区 API
- admin: {
- organizations: () => request<{ organizations: any[] }>('/admin/organizations'),
- createOrganization: (name: string, slug: string) => request<any>('/admin/organizations', {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, slug })
- }),
- organization: (id: string) => request<any>(`/admin/organizations/${id}`),
- renameOrganization: (id: string, name: string) => request<any>(`/admin/organizations/${id}`, {
- method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name })
- }),
- updateOrganizationSlug: (id: string, slug: string) => request<any>(`/admin/organizations/${id}`, {
- method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug })
- }),
- setOrganizationActive: (id: string, isActive: boolean) => request<any>(`/admin/organizations/${id}`, {
- method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isActive })
- }),
- setOrganizationMember: (id: string, userId: string, role: 'MEMBER' | 'MANAGER') => request<any>(`/admin/organizations/${id}/members/${userId}`, {
- method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role })
- }),
- removeOrganizationMember: (id: string, userId: string) => request<any>(`/admin/organizations/${id}/members/${userId}`, { method: 'DELETE' }),
- reviewOrganizationApplication: (id: string, applicationId: string, approve: boolean) => request<any>(`/admin/organizations/${id}/applications/${applicationId}/review`, {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ approve })
- }),
- adjustOrganizationPoints: (id: string, amount: number, reason: string) => request<any>(`/admin/organizations/${id}/points`, {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount, reason })
- }),
- login: (phone: string, password: string) =>
- request<{ token: string; user: User }>('/admin/auth/login', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ phone, password })
- }),
- getStats: () => request<AdminStats>('/admin/stats'),
- listCourses: (params?: { status?: string; keyword?: string }) => {
- const q = new URLSearchParams()
- if (params?.status) q.set('status', params.status)
- if (params?.keyword) q.set('keyword', params.keyword)
- const qs = q.toString() ? `?${q.toString()}` : ''
- return request<{ courses: Course[]; total: number }>(`/admin/courses${qs}`)
- },
- getCourse: (id: string) => request<{ course: Course }>(`/admin/courses/${id}`),
- updateStatus: (id: string, status: CourseStatus) =>
- request<{ course: Course }>(`/admin/courses/${id}/status`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ status })
- }),
- updatePoints: (id: string, points: number, reason: string) =>
- request<{ course: Course }>(`/admin/courses/${id}/points`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ points, reason })
- }),
- uploadDeliverables: (id: string, files: File[], episodeId?: string) => {
- const form = new FormData()
- files.forEach((f) => form.append('deliverables', f))
- if (episodeId) form.append('episodeId', episodeId)
- return request<{ course: Course }>(`/admin/courses/${id}/deliverables`, { method: 'POST', body: form })
- },
- deleteDeliverable: (id: string, delivId: string) =>
- request<{ course: Course }>(`/admin/courses/${id}/deliverables/${delivId}`, { method: 'DELETE' }),
- completeCourse: (id: string, productionNotes: string) =>
- request<{ course: Course }>(`/admin/courses/${id}/complete`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ productionNotes })
- }),
- listUsers: (keyword?: string) => {
- const qs = keyword ? `?keyword=${encodeURIComponent(keyword)}` : ''
- return request<{ users: User[] }>(`/admin/users${qs}`)
- },
- createUser: (body: { phone: string; password: string; contactName?: string }) =>
- request<{ user: User }>('/admin/users', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- }),
- resetUserPassword: (id: string, password: string) =>
- request<{ ok: boolean }>(`/admin/users/${id}/password`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ password })
- }),
- // 广告落地页线索
- listLeads: (params?: { status?: LeadStatus | ''; keyword?: string }) => {
- const q = new URLSearchParams()
- if (params?.status) q.set('status', params.status)
- if (params?.keyword) q.set('keyword', params.keyword)
- const qs = q.toString() ? `?${q.toString()}` : ''
- return request<{ leads: Lead[] }>(`/admin/leads${qs}`)
- },
- updateLead: (id: string, body: { status?: LeadStatus; note?: string }) =>
- request<{ lead: Lead }>(`/admin/leads/${id}`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(body)
- })
- }
- }
|