|
@@ -1,10 +1,11 @@
|
|
|
-import type { AdminStats, Course, CourseStatus, CourseTemplate, PointsTransaction, User } from './types'
|
|
|
|
|
|
|
+import type { AdminStats, Course, CourseStatus, CourseTemplate, DigitalInstructor, Organization, PointsTransaction, User } from './types'
|
|
|
|
|
|
|
|
const token = () => localStorage.getItem('xinghen_token')
|
|
const token = () => localStorage.getItem('xinghen_token')
|
|
|
|
|
|
|
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
|
const headers = new Headers(init.headers)
|
|
const headers = new Headers(init.headers)
|
|
|
if (token()) headers.set('Authorization', `Bearer ${token()}`)
|
|
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 })
|
|
const response = await fetch(`/api${path}`, { ...init, headers })
|
|
|
if (!response.ok) {
|
|
if (!response.ok) {
|
|
|
const body = await response.json().catch(() => ({ message: '请求失败,请稍后重试' }))
|
|
const body = await response.json().catch(() => ({ message: '请求失败,请稍后重试' }))
|
|
@@ -14,16 +15,94 @@ async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
|
|
}
|
|
}
|
|
|
throw new Error(body.message || '请求失败,请稍后重试')
|
|
throw new Error(body.message || '请求失败,请稍后重试')
|
|
|
}
|
|
}
|
|
|
- return response.json() as Promise<T>
|
|
|
|
|
|
|
+ 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 const api = {
|
|
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 })
|
|
|
|
|
+ }),
|
|
|
|
|
+ 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) =>
|
|
|
|
|
|
|
+ register: (phone: string, password: string, contactName: string, inviteToken?: string) =>
|
|
|
request<{ token: string; user: User }>('/auth/register', {
|
|
request<{ token: string; user: User }>('/auth/register', {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ phone, password })
|
|
|
|
|
|
|
+ body: JSON.stringify({ phone, password, contactName, inviteToken })
|
|
|
}),
|
|
}),
|
|
|
login: (phone: string, password: string) =>
|
|
login: (phone: string, password: string) =>
|
|
|
request<{ token: string; user: User }>('/auth/login', {
|
|
request<{ token: string; user: User }>('/auth/login', {
|
|
@@ -35,7 +114,7 @@ export const api = {
|
|
|
me: () => request<{ user: User }>('/me'),
|
|
me: () => request<{ user: User }>('/me'),
|
|
|
getPointsTransactions: () =>
|
|
getPointsTransactions: () =>
|
|
|
request<{ balance: number; transactions: PointsTransaction[] }>('/me/points-transactions'),
|
|
request<{ balance: number; transactions: PointsTransaction[] }>('/me/points-transactions'),
|
|
|
- updateMe: (body: Pick<User, 'organization' | 'wechat' | 'contactName' | 'bio'>) =>
|
|
|
|
|
|
|
+ updateMe: (body: Pick<User, 'wechat' | 'contactName' | 'bio'>) =>
|
|
|
request<{ user: User }>('/me', {
|
|
request<{ user: User }>('/me', {
|
|
|
method: 'PATCH',
|
|
method: 'PATCH',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -59,14 +138,14 @@ export const api = {
|
|
|
|
|
|
|
|
// 普通用户课程操作
|
|
// 普通用户课程操作
|
|
|
listCourses: () => request<{ courses: Course[] }>('/courses'),
|
|
listCourses: () => request<{ courses: Course[] }>('/courses'),
|
|
|
- createCourse: (body: Pick<Course, 'name' | 'category' | 'description'>) =>
|
|
|
|
|
|
|
+ createCourse: (body: Pick<Course, 'name' | 'category' | 'description'> & { digitalInstructorId?: string | null }) =>
|
|
|
request<{ course: Course }>('/courses', {
|
|
request<{ course: Course }>('/courses', {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify(body)
|
|
body: JSON.stringify(body)
|
|
|
}),
|
|
}),
|
|
|
getCourse: (id: string) => request<{ course: Course }>(`/courses/${id}`),
|
|
getCourse: (id: string) => request<{ course: Course }>(`/courses/${id}`),
|
|
|
- updateCourse: (id: string, body: Pick<Course, 'name' | 'category' | 'description'>) =>
|
|
|
|
|
|
|
+ updateCourse: (id: string, body: Pick<Course, 'name' | 'category' | 'description'> & { digitalInstructorId?: string | null }) =>
|
|
|
request<{ course: Course }>(`/courses/${id}`, {
|
|
request<{ course: Course }>(`/courses/${id}`, {
|
|
|
method: 'PATCH',
|
|
method: 'PATCH',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -133,6 +212,30 @@ export const api = {
|
|
|
|
|
|
|
|
// 管理员后台专区 API
|
|
// 管理员后台专区 API
|
|
|
admin: {
|
|
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) =>
|
|
login: (phone: string, password: string) =>
|
|
|
request<{ token: string; user: User }>('/admin/auth/login', {
|
|
request<{ token: string; user: User }>('/admin/auth/login', {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
@@ -178,7 +281,7 @@ export const api = {
|
|
|
const qs = keyword ? `?keyword=${encodeURIComponent(keyword)}` : ''
|
|
const qs = keyword ? `?keyword=${encodeURIComponent(keyword)}` : ''
|
|
|
return request<{ users: User[] }>(`/admin/users${qs}`)
|
|
return request<{ users: User[] }>(`/admin/users${qs}`)
|
|
|
},
|
|
},
|
|
|
- createUser: (body: { phone: string; password: string; contactName?: string; organization?: string }) =>
|
|
|
|
|
|
|
+ createUser: (body: { phone: string; password: string; contactName?: string }) =>
|
|
|
request<{ user: User }>('/admin/users', {
|
|
request<{ user: User }>('/admin/users', {
|
|
|
method: 'POST',
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -189,12 +292,6 @@ export const api = {
|
|
|
method: 'PATCH',
|
|
method: 'PATCH',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ password })
|
|
body: JSON.stringify({ password })
|
|
|
- }),
|
|
|
|
|
- updateUserRole: (id: string, role: 'USER' | 'ADMIN') =>
|
|
|
|
|
- request<{ user: User }>(`/admin/users/${id}/role`, {
|
|
|
|
|
- method: 'PATCH',
|
|
|
|
|
- headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
- body: JSON.stringify({ role })
|
|
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|