1173 lines
37 KiB
TypeScript
1173 lines
37 KiB
TypeScript
// 课程相关API接口
|
||
import { ApiRequest } from '../request'
|
||
import type {
|
||
ApiResponse,
|
||
PaginationResponse,
|
||
Course,
|
||
CourseCategory,
|
||
CourseSubject,
|
||
CourseDifficulty,
|
||
CourseListQueryParams,
|
||
BackendCourseItem,
|
||
Chapter,
|
||
Lesson,
|
||
LessonResource,
|
||
CourseSection,
|
||
CourseSectionListResponse,
|
||
BackendCourseSection,
|
||
BackendInstructor,
|
||
BackendSectionVideo,
|
||
BackendComment,
|
||
SectionVideo,
|
||
VideoQuality,
|
||
CourseComment,
|
||
Quiz,
|
||
LearningProgress,
|
||
SearchRequest,
|
||
Instructor,
|
||
} from '../types'
|
||
|
||
/**
|
||
* 课程API模块
|
||
*/
|
||
export class CourseApi {
|
||
|
||
/**
|
||
* 格式化时间戳为ISO字符串
|
||
*/
|
||
private static formatTimestamp(timestamp: number | string | null | undefined): string {
|
||
if (!timestamp) {
|
||
return new Date().toISOString()
|
||
}
|
||
|
||
try {
|
||
// 如果是字符串,尝试解析
|
||
if (typeof timestamp === 'string') {
|
||
const date = new Date(timestamp)
|
||
return isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString()
|
||
}
|
||
|
||
// 如果是数字时间戳
|
||
if (timestamp <= 0) {
|
||
return new Date().toISOString()
|
||
}
|
||
|
||
// 如果时间戳是秒级的,转换为毫秒级
|
||
const ms = timestamp < 10000000000 ? timestamp * 1000 : timestamp
|
||
const date = new Date(ms)
|
||
|
||
// 检查日期是否有效
|
||
if (isNaN(date.getTime())) {
|
||
return new Date().toISOString()
|
||
}
|
||
|
||
return date.toISOString()
|
||
} catch (error) {
|
||
console.error('时间戳格式化失败:', error)
|
||
return new Date().toISOString()
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 映射后端难度值到前端级别
|
||
*/
|
||
private static mapDifficultyToLevel(difficulty: number): string {
|
||
switch (difficulty) {
|
||
case 0: return '零基础'
|
||
case 1: return '初级'
|
||
case 2: return '进阶'
|
||
case 3: return '高阶'
|
||
default: return '未知'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 映射后端难度值到标准级别
|
||
*/
|
||
private static mapDifficultyToStandardLevel(difficulty: number): 'beginner' | 'intermediate' | 'advanced' {
|
||
switch (difficulty) {
|
||
case 0: return 'beginner'
|
||
case 1: return 'beginner'
|
||
case 2: return 'intermediate'
|
||
case 3: return 'advanced'
|
||
default: return 'beginner'
|
||
}
|
||
}
|
||
// 获取课程列表 - 适配后端接口
|
||
static async getCourses(params?: CourseListQueryParams): Promise<ApiResponse<Course[]>> {
|
||
try {
|
||
console.log('🚀 调用课程列表API,参数:', params)
|
||
|
||
// 构建查询参数,根据API文档的参数名称
|
||
const queryParams: any = {}
|
||
if (params?.categoryId) queryParams.categoryId = params.categoryId
|
||
if (params?.difficulty) queryParams.difficulty = params.difficulty
|
||
if (params?.subject) queryParams.subject = params.subject
|
||
|
||
console.log('🔍 查询参数:', queryParams)
|
||
|
||
// 调用后端API
|
||
const response = await ApiRequest.get<any>('/aiol/aiolCourse/query_list', queryParams)
|
||
console.log('🔍 课程列表API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success && response.data.result) {
|
||
// 转换后端数据格式为前端格式
|
||
const courses: Course[] = response.data.result.map((item: BackendCourseItem) => ({
|
||
id: item.id, // 保持字符串格式,不转换为数字
|
||
title: item.name || '',
|
||
description: item.description || '',
|
||
instructor: item.school || '未知讲师',
|
||
teacherList: item.teacherList || [], // 新增:传递讲师列表
|
||
duration: item.arrangement || '待定',
|
||
level: this.mapDifficultyToLevel(item.difficulty),
|
||
category: item.subject || '其他',
|
||
thumbnail: item.cover || '',
|
||
price: 0, // 后端没有价格字段,设为0
|
||
rating: 0, // 后端没有评分字段,设为0
|
||
studentsCount: item.enrollCount || 0,
|
||
lessonsCount: 0, // 后端没有课程数量字段,设为0
|
||
tags: [],
|
||
isEnrolled: false,
|
||
progress: 0,
|
||
createdAt: this.formatTimestamp(item.createTime),
|
||
updatedAt: this.formatTimestamp(item.updateTime),
|
||
status: item.status === 1 ? 'published' : 'draft',
|
||
enrollmentCount: item.enrollCount || 0,
|
||
maxEnrollment: item.maxEnroll || 0,
|
||
startDate: item.startTime || '',
|
||
endDate: item.endTime || '',
|
||
outline: item.outline || '',
|
||
prerequisite: item.prerequisite || '',
|
||
reference: item.reference || '',
|
||
target: item.target || '',
|
||
question: item.question || '',
|
||
video: item.video || ''
|
||
}))
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: courses
|
||
}
|
||
} else {
|
||
console.warn('⚠️ 课程列表API返回格式异常:', response)
|
||
return {
|
||
code: 500,
|
||
message: response.data?.message || '获取课程列表失败',
|
||
data: []
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('课程API调用失败:', error)
|
||
|
||
let errorMessage = '获取课程列表失败'
|
||
if (error.code === 'ECONNABORTED') {
|
||
errorMessage = '请求超时,请检查网络连接'
|
||
} else if (error.message === 'Network Error') {
|
||
errorMessage = '网络连接失败,请检查网络设置'
|
||
} else if (error.message?.includes('网络')) {
|
||
errorMessage = error.message
|
||
}
|
||
|
||
// 返回空数据而不是抛出错误,确保应用不会崩溃
|
||
return {
|
||
code: 500,
|
||
message: errorMessage,
|
||
data: []
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取课程详情 - 适配后端接口
|
||
static async getCourseDetail(id: string): Promise<ApiResponse<Course>> {
|
||
try {
|
||
console.log('🚀 调用课程详情API,课程ID:', id)
|
||
|
||
// 调用后端API
|
||
const response = await ApiRequest.get<any>('/aiol/aiolCourse/detail', { id })
|
||
|
||
console.log('🔍 课程详情API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success) {
|
||
// 检查result是否为null或空
|
||
if (!response.data.result) {
|
||
console.warn('⚠️ 课程详情为空,可能课程不存在或已删除')
|
||
return {
|
||
code: 404,
|
||
message: '课程不存在或已删除',
|
||
data: {} as Course
|
||
}
|
||
}
|
||
// 转换后端数据格式为前端格式
|
||
const item: BackendCourseItem = response.data.result
|
||
const course: Course = {
|
||
id: item.id, // 保持字符串格式,不转换为数字
|
||
title: item.name || '',
|
||
description: item.description || '',
|
||
thumbnail: item.cover || '',
|
||
price: 0, // 后端没有价格字段,设为0
|
||
currency: 'CNY',
|
||
rating: 0, // 后端没有评分字段,设为0
|
||
ratingCount: 0,
|
||
studentsCount: item.enrollCount || 0,
|
||
duration: item.arrangement || '待定',
|
||
totalLessons: 0,
|
||
level: this.mapDifficultyToStandardLevel(item.difficulty),
|
||
language: 'zh-CN',
|
||
category: {
|
||
id: 1,
|
||
name: item.subject || '其他',
|
||
slug: 'other'
|
||
},
|
||
tags: [],
|
||
skills: [],
|
||
requirements: item.prerequisite ? [item.prerequisite] : [],
|
||
objectives: item.target ? [item.target] : [],
|
||
instructor: {
|
||
id: 1,
|
||
name: item.school || '未知讲师',
|
||
title: '讲师',
|
||
bio: '',
|
||
avatar: '',
|
||
rating: 4.5,
|
||
studentsCount: 0,
|
||
coursesCount: 0,
|
||
experience: '',
|
||
education: [],
|
||
certifications: []
|
||
},
|
||
status: item.status === 1 ? 'published' : 'draft',
|
||
isEnrolled: false,
|
||
progress: 0,
|
||
createdAt: this.formatTimestamp(item.createTime),
|
||
updatedAt: this.formatTimestamp(item.updateTime)
|
||
}
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: course
|
||
}
|
||
} else {
|
||
console.warn('⚠️ 课程详情API返回格式异常:', response)
|
||
return {
|
||
code: 500,
|
||
message: response.data?.message || '获取课程详情失败',
|
||
data: {} as Course
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ 获取课程详情失败:', error)
|
||
return {
|
||
code: 500,
|
||
message: error.message || '获取课程详情失败',
|
||
data: {} as Course
|
||
}
|
||
}
|
||
}
|
||
|
||
// 搜索课程
|
||
static searchCourses(params: SearchRequest): Promise<ApiResponse<PaginationResponse<Course>>> {
|
||
return ApiRequest.get('/courses/search', params)
|
||
}
|
||
|
||
// 获取热门课程
|
||
static getPopularCourses(limit?: number): Promise<ApiResponse<Course[]>> {
|
||
return ApiRequest.get('/courses/popular', { limit })
|
||
}
|
||
|
||
// 获取最新课程
|
||
static getLatestCourses(limit?: number): Promise<ApiResponse<Course[]>> {
|
||
return ApiRequest.get('/courses/latest', { limit })
|
||
}
|
||
|
||
// 获取推荐课程
|
||
static getRecommendedCourses(userId?: number, limit?: number): Promise<ApiResponse<Course[]>> {
|
||
return ApiRequest.get('/courses/recommended', { userId, limit })
|
||
}
|
||
|
||
// 获取课程详情 - 适配后端接口
|
||
static async getCourseById(id: string): Promise<ApiResponse<Course>> {
|
||
try {
|
||
// 调用后端课程详情接口
|
||
const response = await ApiRequest.get<any>('/aiol/aiolCourse/detail', { id })
|
||
|
||
console.log('🔍 课程详情API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success) {
|
||
// 检查result是否为null或空
|
||
if (!response.data.result) {
|
||
console.warn('⚠️ 课程详情为空,可能课程不存在或已删除')
|
||
return {
|
||
code: 404,
|
||
message: '课程不存在或已删除',
|
||
data: {} as Course
|
||
}
|
||
}
|
||
// 转换后端数据格式为前端格式
|
||
const item: BackendCourseItem = response.data.result
|
||
const course: Course = {
|
||
id: item.id, // 保持字符串格式,不转换为数字
|
||
title: item.name || '',
|
||
description: item.description || '',
|
||
thumbnail: item.cover || '',
|
||
price: 0, // 后端没有价格字段,设为0
|
||
currency: 'CNY',
|
||
rating: 0, // 后端没有评分字段,设为0
|
||
ratingCount: 0,
|
||
studentsCount: item.enrollCount || 0,
|
||
duration: item.arrangement || '待定',
|
||
totalLessons: 0,
|
||
level: this.mapDifficultyToStandardLevel(item.difficulty),
|
||
language: 'zh-CN',
|
||
category: {
|
||
id: 1,
|
||
name: item.subject || '其他',
|
||
slug: 'other'
|
||
},
|
||
tags: [],
|
||
skills: [],
|
||
requirements: item.prerequisite ? [item.prerequisite] : [],
|
||
objectives: item.target ? [item.target] : [],
|
||
instructor: {
|
||
id: 1,
|
||
name: item.school || '未知讲师',
|
||
title: '讲师',
|
||
bio: '',
|
||
avatar: '',
|
||
rating: 4.5,
|
||
studentsCount: 0,
|
||
coursesCount: 0,
|
||
experience: '',
|
||
education: [],
|
||
certifications: []
|
||
},
|
||
status: item.status === 1 ? 'published' : 'draft',
|
||
isEnrolled: false,
|
||
progress: 0,
|
||
createdAt: this.formatTimestamp(item.createTime),
|
||
updatedAt: this.formatTimestamp(item.updateTime)
|
||
}
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: course
|
||
}
|
||
} else {
|
||
console.warn('⚠️ 课程详情API返回格式异常:', response)
|
||
return {
|
||
code: 500,
|
||
message: response.data?.message || '获取课程详情失败',
|
||
data: {} as Course
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ 获取课程详情失败:', error)
|
||
return {
|
||
code: 500,
|
||
message: error.message || '获取课程详情失败',
|
||
data: {} as Course
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取课程章节
|
||
static getCourseChapters(courseId: number): Promise<ApiResponse<Chapter[]>> {
|
||
return ApiRequest.get(`/courses/${courseId}/chapters`)
|
||
}
|
||
|
||
// 获取课程所有课时
|
||
static getCourseLessons(courseId: number): Promise<ApiResponse<Lesson[]>> {
|
||
return ApiRequest.get(`/courses/${courseId}/lessons`)
|
||
}
|
||
|
||
// 获取章节详情
|
||
static getChapterById(id: number): Promise<ApiResponse<Chapter>> {
|
||
return ApiRequest.get(`/chapters/${id}`)
|
||
}
|
||
|
||
// 获取课时详情
|
||
static getLessonById(id: number): Promise<ApiResponse<Lesson>> {
|
||
return ApiRequest.get(`/lessons/${id}`)
|
||
}
|
||
|
||
// 获取课时资源
|
||
static getLessonResources(lessonId: number): Promise<ApiResponse<LessonResource[]>> {
|
||
return ApiRequest.get(`/lessons/${lessonId}/resources`)
|
||
}
|
||
|
||
// 获取课程分类
|
||
static async getCategories(): Promise<ApiResponse<CourseCategory[]>> {
|
||
try {
|
||
console.log('🚀 获取课程分类列表')
|
||
|
||
// 调用后端API(不需要token)
|
||
const response = await ApiRequest.get<any>('/aiol/aiolCourse/category/list')
|
||
|
||
console.log('🔍 分类API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success && response.data.result) {
|
||
// 转换后端数据格式为前端格式
|
||
const categories: CourseCategory[] = response.data.result.map((item: any) => ({
|
||
id: parseInt(item.id) || 0,
|
||
name: item.name || '',
|
||
slug: item.name?.toLowerCase().replace(/\s+/g, '-') || '',
|
||
description: '',
|
||
sortOrder: item.sortOrder || 0
|
||
}))
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: categories
|
||
}
|
||
} else {
|
||
console.warn('⚠️ 分类API返回格式异常:', response)
|
||
return {
|
||
code: 500,
|
||
message: response.data?.message || '获取分类失败',
|
||
data: []
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ 获取课程分类失败:', error)
|
||
return {
|
||
code: 500,
|
||
message: error.message || '获取分类失败',
|
||
data: []
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取课程专题列表
|
||
static async getSubjects(): Promise<ApiResponse<CourseSubject[]>> {
|
||
try {
|
||
console.log('🚀 获取课程专题列表')
|
||
|
||
// 调用后端API(不需要token)
|
||
const response = await ApiRequest.get<any>('/aiol/aiolCourse/subject/list')
|
||
|
||
console.log('🔍 专题API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success && response.data.result) {
|
||
// 转换后端数据格式为前端格式(专题接口返回的是value和label字段)
|
||
const subjects: CourseSubject[] = response.data.result.map((item: any, index: number) => ({
|
||
id: item.value || '0', // 保持原始的value值(字符串格式)
|
||
name: item.label || '',
|
||
slug: item.label?.toLowerCase().replace(/\s+/g, '-') || '',
|
||
description: '',
|
||
sortOrder: index
|
||
}))
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: subjects
|
||
}
|
||
} else {
|
||
console.warn('⚠️ 专题API返回格式异常:', response)
|
||
return {
|
||
code: 500,
|
||
message: response.data?.message || '获取专题失败',
|
||
data: []
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ 获取课程专题失败:', error)
|
||
return {
|
||
code: 500,
|
||
message: error.message || '获取专题失败',
|
||
data: []
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取课程难度列表
|
||
static async getDifficulties(): Promise<ApiResponse<CourseDifficulty[]>> {
|
||
try {
|
||
console.log('🚀 获取课程难度列表')
|
||
|
||
// 调用后端API(不需要token)
|
||
const response = await ApiRequest.get<any>('/aiol/aiolCourse/difficulty/list')
|
||
|
||
console.log('🔍 难度API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success && response.data.result) {
|
||
// 转换后端数据格式为前端格式(难度接口返回的是value和label字段)
|
||
const difficulties: CourseDifficulty[] = response.data.result.map((item: any, index: number) => ({
|
||
id: item.value || '0', // 保持原始的value值(字符串格式)
|
||
name: item.label || '',
|
||
slug: item.label?.toLowerCase().replace(/\s+/g, '-') || '',
|
||
description: '',
|
||
sortOrder: index
|
||
}))
|
||
|
||
return {
|
||
code: 200,
|
||
message: '获取成功',
|
||
data: difficulties
|
||
}
|
||
} else {
|
||
console.warn('⚠️ 难度API返回格式异常:', response)
|
||
return {
|
||
code: 500,
|
||
message: response.data?.message || '获取难度失败',
|
||
data: []
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ 获取课程难度失败:', error)
|
||
return {
|
||
code: 500,
|
||
message: error.message || '获取难度失败',
|
||
data: []
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取分类下的课程
|
||
static getCoursesByCategory(categoryId: number, params?: {
|
||
page?: number
|
||
pageSize?: number
|
||
sortBy?: string
|
||
}): Promise<ApiResponse<PaginationResponse<Course>>> {
|
||
return ApiRequest.get(`/categories/${categoryId}/courses`, params)
|
||
}
|
||
|
||
// 报名课程
|
||
static async enrollCourse(courseId: string): Promise<ApiResponse<{
|
||
success: boolean
|
||
message: string
|
||
result?: any
|
||
}>> {
|
||
try {
|
||
console.log('🔍 报名课程,课程ID:', courseId)
|
||
console.log('🔍 API请求URL: /aiol/aiolCourse/' + courseId + '/enroll')
|
||
|
||
const response = await ApiRequest.post<any>(`/aiol/aiolCourse/${courseId}/enroll`)
|
||
console.log('🔍 报名API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && typeof response.data === 'object') {
|
||
// 如果响应包含success字段,使用标准格式
|
||
if ('success' in response.data) {
|
||
return {
|
||
code: response.data.code || 0,
|
||
message: response.data.message || '报名成功',
|
||
data: {
|
||
success: response.data.success || false,
|
||
message: response.data.message || '',
|
||
result: response.data
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 默认返回成功状态
|
||
return {
|
||
code: 0,
|
||
message: '报名成功',
|
||
data: {
|
||
success: true,
|
||
message: '报名成功',
|
||
result: null
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ 课程报名失败:', error)
|
||
|
||
// 如果是401错误,说明未登录
|
||
if (error.response?.status === 401) {
|
||
return {
|
||
code: 401,
|
||
message: '请先登录',
|
||
data: {
|
||
success: false,
|
||
message: '请先登录',
|
||
result: null
|
||
}
|
||
}
|
||
}
|
||
|
||
// 其他错误
|
||
return {
|
||
code: error.response?.status || 500,
|
||
message: error.message || '报名失败',
|
||
data: {
|
||
success: false,
|
||
message: error.message || '报名失败',
|
||
result: null
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 取消报名
|
||
static unenrollCourse(courseId: number): Promise<ApiResponse<null>> {
|
||
return ApiRequest.delete(`/courses/${courseId}/enroll`)
|
||
}
|
||
|
||
// 检查课程报名状态
|
||
static async checkEnrollmentStatus(courseId: string): Promise<ApiResponse<{
|
||
result: boolean
|
||
message?: string
|
||
}>> {
|
||
try {
|
||
console.log('🔍 检查课程报名状态,课程ID:', courseId)
|
||
console.log('🔍 API请求URL: /aiol/aiolCourse/' + courseId + '/is_enrolled')
|
||
|
||
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${courseId}/is_enrolled`)
|
||
console.log('🔍 报名状态API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && typeof response.data === 'object') {
|
||
// 如果响应包含success字段,使用标准格式
|
||
if ('success' in response.data) {
|
||
return {
|
||
code: response.data.code || 0,
|
||
message: response.data.message || '',
|
||
data: {
|
||
result: response.data.result || false,
|
||
message: response.data.message
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果直接返回result字段
|
||
if ('result' in response.data) {
|
||
return {
|
||
code: 0,
|
||
message: '查询成功',
|
||
data: {
|
||
result: response.data.result || false,
|
||
message: response.data.message
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 默认返回未报名状态
|
||
return {
|
||
code: 0,
|
||
message: '查询成功',
|
||
data: {
|
||
result: false,
|
||
message: '未报名'
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('❌ 检查课程报名状态失败:', error)
|
||
|
||
// 如果是401错误,说明未登录
|
||
if (error.response?.status === 401) {
|
||
return {
|
||
code: 401,
|
||
message: '请先登录',
|
||
data: {
|
||
result: false,
|
||
message: '请先登录'
|
||
}
|
||
}
|
||
}
|
||
|
||
// 其他错误,默认返回未报名状态
|
||
return {
|
||
code: error.response?.status || 500,
|
||
message: error.message || '查询失败',
|
||
data: {
|
||
result: false,
|
||
message: error.message || '查询失败'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取课程章节列表
|
||
static async getCourseSections(courseId: string): Promise<ApiResponse<CourseSectionListResponse>> {
|
||
try {
|
||
console.log('🔍 获取课程章节数据,课程ID:', courseId)
|
||
console.log('🔍 API请求URL: /aiol/aiolCourse/' + courseId + '/section')
|
||
|
||
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${courseId}/section`)
|
||
console.log('🔍 章节API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success && response.data.result) {
|
||
console.log('✅ 响应状态码:', response.data.code)
|
||
console.log('✅ 响应消息:', response.data.message)
|
||
console.log('✅ 原始章节数据:', response.data.result)
|
||
console.log('✅ 章节数据数量:', response.data.result.length || 0)
|
||
|
||
// 适配数据格式
|
||
const adaptedSections: CourseSection[] = response.data.result.map((section: BackendCourseSection) => ({
|
||
id: section.id,
|
||
lessonId: section.courseId, // 使用courseId作为lessonId
|
||
outline: '', // 暂时为空,根据type可以设置不同的内容
|
||
name: section.name,
|
||
type: section.type, // 保持原值,可能为null
|
||
parentId: section.parentId || '', // 如果parentId为空字符串,保持为空字符串
|
||
sort: section.sortOrder,
|
||
level: section.level,
|
||
revision: 1, // 默认版本号
|
||
createdAt: section.createTime ? new Date(section.createTime).getTime() : null,
|
||
updatedAt: section.updateTime ? new Date(section.updateTime).getTime() : null,
|
||
deletedAt: null,
|
||
completed: false,
|
||
duration: undefined
|
||
}))
|
||
|
||
console.log('✅ 适配后的章节数据:', adaptedSections)
|
||
|
||
return {
|
||
code: response.data.code,
|
||
message: response.data.message,
|
||
data: {
|
||
list: adaptedSections,
|
||
timestamp: Date.now(),
|
||
traceId: response.data.timestamp?.toString() || ''
|
||
}
|
||
}
|
||
} else {
|
||
console.warn('⚠️ API返回的数据结构不正确:', response.data)
|
||
return {
|
||
code: 500,
|
||
message: '数据格式错误',
|
||
data: {
|
||
list: [],
|
||
timestamp: Date.now(),
|
||
traceId: ''
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 章节API调用失败:', error)
|
||
console.error('❌ 错误详情:', {
|
||
message: (error as Error).message,
|
||
stack: (error as Error).stack,
|
||
response: (error as any).response?.data,
|
||
status: (error as any).response?.status,
|
||
statusText: (error as any).response?.statusText
|
||
})
|
||
|
||
// 重新抛出错误,不使用模拟数据
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 获取我的课程
|
||
static getMyCourses(params?: {
|
||
page?: number
|
||
pageSize?: number
|
||
status?: 'all' | 'in_progress' | 'completed'
|
||
}): Promise<ApiResponse<PaginationResponse<Course>>> {
|
||
return ApiRequest.get('/my-courses', params)
|
||
}
|
||
|
||
// 获取学习进度
|
||
static getLearningProgress(courseId: number): Promise<ApiResponse<LearningProgress>> {
|
||
return ApiRequest.get(`/courses/${courseId}/progress`)
|
||
}
|
||
|
||
// 更新学习进度
|
||
static updateLearningProgress(data: {
|
||
courseId: number
|
||
lessonId: number
|
||
progress: number
|
||
timeSpent?: number
|
||
}): Promise<ApiResponse<LearningProgress>> {
|
||
return ApiRequest.post('/learning-progress', data)
|
||
}
|
||
|
||
// 标记课时完成
|
||
static markLessonCompleted(lessonId: number): Promise<ApiResponse<null>> {
|
||
return ApiRequest.post(`/lessons/${lessonId}/complete`)
|
||
}
|
||
|
||
// 获取课程测验
|
||
static getCourseQuizzes(courseId: number): Promise<ApiResponse<Quiz[]>> {
|
||
return ApiRequest.get(`/courses/${courseId}/quizzes`)
|
||
}
|
||
|
||
// 获取测验详情
|
||
static getQuizById(id: number): Promise<ApiResponse<Quiz>> {
|
||
return ApiRequest.get(`/quizzes/${id}`)
|
||
}
|
||
|
||
// 提交测验答案
|
||
static submitQuizAnswers(quizId: number, answers: Array<{
|
||
questionId: number
|
||
answer: string | string[]
|
||
}>): Promise<ApiResponse<{
|
||
score: number
|
||
totalScore: number
|
||
passed: boolean
|
||
correctAnswers: number
|
||
totalQuestions: number
|
||
results: Array<{
|
||
questionId: number
|
||
correct: boolean
|
||
userAnswer: string | string[]
|
||
correctAnswer: string | string[]
|
||
}>
|
||
}>> {
|
||
return ApiRequest.post(`/quizzes/${quizId}/submit`, { answers })
|
||
}
|
||
|
||
// 获取测验结果
|
||
static getQuizResults(quizId: number): Promise<ApiResponse<{
|
||
attempts: Array<{
|
||
id: number
|
||
score: number
|
||
totalScore: number
|
||
passed: boolean
|
||
submittedAt: string
|
||
}>
|
||
bestScore: number
|
||
averageScore: number
|
||
totalAttempts: number
|
||
}>> {
|
||
return ApiRequest.get(`/quizzes/${quizId}/results`)
|
||
}
|
||
|
||
// 下载课程资源
|
||
static downloadResource(resourceId: number): Promise<void> {
|
||
return ApiRequest.download(`/resources/${resourceId}/download`)
|
||
}
|
||
|
||
// 获取讲师信息
|
||
static getInstructorById(id: number): Promise<ApiResponse<Instructor>> {
|
||
return ApiRequest.get(`/instructors/${id}`)
|
||
}
|
||
|
||
// 获取讲师的课程
|
||
static getInstructorCourses(instructorId: number, params?: {
|
||
page?: number
|
||
pageSize?: number
|
||
}): Promise<ApiResponse<PaginationResponse<Course>>> {
|
||
return ApiRequest.get(`/instructors/${instructorId}/courses`, params)
|
||
}
|
||
|
||
// 关注讲师
|
||
static followInstructor(instructorId: number): Promise<ApiResponse<null>> {
|
||
return ApiRequest.post(`/instructors/${instructorId}/follow`)
|
||
}
|
||
|
||
// 取消关注讲师
|
||
static unfollowInstructor(instructorId: number): Promise<ApiResponse<null>> {
|
||
return ApiRequest.delete(`/instructors/${instructorId}/follow`)
|
||
}
|
||
|
||
// 获取课程统计信息
|
||
static getCourseStats(courseId: number): Promise<ApiResponse<{
|
||
totalStudents: number
|
||
totalLessons: number
|
||
totalDuration: string
|
||
averageRating: number
|
||
completionRate: number
|
||
enrollmentTrend: Array<{
|
||
date: string
|
||
count: number
|
||
}>
|
||
}>> {
|
||
return ApiRequest.get(`/courses/${courseId}/stats`)
|
||
}
|
||
|
||
// 预览课程(免费课时)
|
||
static previewCourse(courseId: number): Promise<ApiResponse<{
|
||
freeLessons: Lesson[]
|
||
previewVideo?: string
|
||
}>> {
|
||
return ApiRequest.get(`/courses/${courseId}/preview`)
|
||
}
|
||
|
||
// 获取相关课程推荐
|
||
static getRelatedCourses(courseId: number, limit?: number): Promise<ApiResponse<Course[]>> {
|
||
return ApiRequest.get(`/courses/${courseId}/related`, { limit })
|
||
}
|
||
|
||
// 检查课程访问权限
|
||
static checkCourseAccess(courseId: number): Promise<ApiResponse<{
|
||
hasAccess: boolean
|
||
reason?: string
|
||
expiresAt?: string
|
||
}>> {
|
||
return ApiRequest.get(`/courses/${courseId}/access`)
|
||
}
|
||
|
||
// 获取课程讲师列表
|
||
static async getCourseInstructors(courseId: string): Promise<ApiResponse<Instructor[]>> {
|
||
try {
|
||
console.log('🔍 获取课程讲师数据,课程ID:', courseId)
|
||
console.log('🔍 API请求URL: /aiol/aiolCourse/' + courseId + '/teachers')
|
||
|
||
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${courseId}/teachers`)
|
||
console.log('🔍 讲师API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success && response.data.result) {
|
||
console.log('✅ 响应状态码:', response.data.code)
|
||
console.log('✅ 响应消息:', response.data.message)
|
||
console.log('✅ 原始讲师数据:', response.data.result)
|
||
console.log('✅ 讲师数据数量:', response.data.result.length || 0)
|
||
|
||
// 适配数据格式
|
||
const adaptedInstructors: Instructor[] = response.data.result.map((instructor: BackendInstructor) => ({
|
||
id: parseInt(instructor.id) || 0, // 转换为数字ID
|
||
name: instructor.name,
|
||
title: instructor.title,
|
||
bio: instructor.tag || '', // 使用tag作为bio
|
||
avatar: instructor.avatar,
|
||
rating: 4.8, // 默认评分
|
||
studentsCount: 1000, // 默认学生数
|
||
coursesCount: 10, // 默认课程数
|
||
experience: '5年教学经验', // 默认经验
|
||
education: ['计算机科学硕士'], // 默认教育背景
|
||
certifications: ['高级讲师认证'] // 默认认证
|
||
}))
|
||
|
||
console.log('✅ 适配后的讲师数据:', adaptedInstructors)
|
||
|
||
return {
|
||
code: response.data.code,
|
||
message: response.data.message,
|
||
data: adaptedInstructors
|
||
}
|
||
} else {
|
||
console.warn('⚠️ API返回的数据结构不正确:', response.data)
|
||
return {
|
||
code: 500,
|
||
message: '数据格式错误',
|
||
data: []
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 讲师API调用失败:', error)
|
||
console.error('❌ 错误详情:', {
|
||
message: (error as Error).message,
|
||
stack: (error as Error).stack,
|
||
response: (error as any).response?.data,
|
||
status: (error as any).response?.status,
|
||
statusText: (error as any).response?.statusText
|
||
})
|
||
|
||
// 重新抛出错误,不使用模拟数据
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 获取章节视频列表
|
||
static async getSectionVideos(courseId: string, sectionId: string): Promise<ApiResponse<SectionVideo[]>> {
|
||
try {
|
||
console.log('🔍 获取章节视频数据,课程ID:', courseId, '章节ID:', sectionId)
|
||
console.log('🔍 API请求URL: /aiol/aiolCourse/' + courseId + '/section_video/' + sectionId)
|
||
|
||
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${courseId}/section_video/${sectionId}`)
|
||
console.log('🔍 章节视频API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success && response.data.result) {
|
||
console.log('✅ 响应状态码:', response.data.code)
|
||
console.log('✅ 响应消息:', response.data.message)
|
||
console.log('✅ 原始视频数据:', response.data.result)
|
||
console.log('✅ 视频数据数量:', response.data.result.length || 0)
|
||
|
||
// 适配数据格式
|
||
const adaptedVideos: SectionVideo[] = response.data.result.map((video: BackendSectionVideo) => {
|
||
// 解析fileUrl中的多个清晰度URL
|
||
const qualities = this.parseVideoQualities(video.fileUrl)
|
||
|
||
return {
|
||
id: video.id,
|
||
name: video.name,
|
||
description: video.description,
|
||
type: video.type,
|
||
thumbnailUrl: video.thumbnailUrl,
|
||
duration: video.duration,
|
||
fileSize: video.fileSize,
|
||
qualities: qualities,
|
||
defaultQuality: '360', // 默认360p
|
||
currentQuality: '360' // 当前选中360p
|
||
}
|
||
})
|
||
|
||
console.log('✅ 适配后的视频数据:', adaptedVideos)
|
||
|
||
return {
|
||
code: response.data.code,
|
||
message: response.data.message,
|
||
data: adaptedVideos
|
||
}
|
||
} else {
|
||
console.warn('⚠️ API返回的数据结构不正确:', response.data)
|
||
return {
|
||
code: 500,
|
||
message: '数据格式错误',
|
||
data: []
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 章节视频API调用失败:', error)
|
||
console.error('❌ 错误详情:', {
|
||
message: (error as Error).message,
|
||
stack: (error as Error).stack,
|
||
response: (error as any).response?.data,
|
||
status: (error as any).response?.status,
|
||
statusText: (error as any).response?.statusText
|
||
})
|
||
|
||
// 重新抛出错误,不使用模拟数据
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 解析视频URL(逗号分隔的多个清晰度URL)
|
||
private static parseVideoQualities(fileUrl: string): VideoQuality[] {
|
||
const qualities: VideoQuality[] = []
|
||
|
||
try {
|
||
if (!fileUrl || fileUrl.trim() === '') {
|
||
console.warn('视频URL为空')
|
||
return qualities
|
||
}
|
||
|
||
// 按逗号分割URL
|
||
const urls = fileUrl.split(',').map(url => url.trim()).filter(url => url.length > 0)
|
||
console.log('🔍 分割后的视频URL:', urls)
|
||
|
||
// 支持的清晰度列表(按优先级排序)
|
||
const supportedQualities = [
|
||
{ value: '1080', label: '1080p' },
|
||
{ value: '720', label: '720p' },
|
||
{ value: '480', label: '480p' },
|
||
{ value: '360', label: '360p' }
|
||
]
|
||
|
||
// 根据URL数量分配清晰度
|
||
// 假设URL按清晰度从高到低排列:1080p, 720p, 480p, 360p
|
||
urls.forEach((url, index) => {
|
||
if (index < supportedQualities.length) {
|
||
qualities.push({
|
||
label: supportedQualities[index].label,
|
||
value: supportedQualities[index].value,
|
||
url: url
|
||
})
|
||
}
|
||
})
|
||
|
||
// 如果没有解析到任何清晰度,使用第一个URL作为360p
|
||
if (qualities.length === 0 && urls.length > 0) {
|
||
qualities.push({
|
||
label: '360p',
|
||
value: '360',
|
||
url: urls[0]
|
||
})
|
||
}
|
||
|
||
console.log('✅ 解析后的视频清晰度:', qualities)
|
||
} catch (error) {
|
||
console.warn('解析视频清晰度失败,使用原始URL:', error)
|
||
// 如果解析失败,将整个fileUrl作为360p使用
|
||
qualities.push({
|
||
label: '360p',
|
||
value: '360',
|
||
url: fileUrl
|
||
})
|
||
}
|
||
|
||
return qualities
|
||
}
|
||
|
||
// 获取课程评论列表
|
||
static async getCourseComments(courseId: string): Promise<ApiResponse<CourseComment[]>> {
|
||
try {
|
||
console.log('🔍 获取课程评论数据,课程ID:', courseId)
|
||
console.log('🔍 API请求URL: /aiol/aiolComment/course/' + courseId + '/list')
|
||
|
||
const response = await ApiRequest.get<any>(`/aiol/aiolComment/course/${courseId}/list`)
|
||
console.log('🔍 评论API响应:', response)
|
||
|
||
// 处理后端响应格式
|
||
if (response.data && response.data.success && response.data.result) {
|
||
console.log('✅ 响应状态码:', response.data.code)
|
||
console.log('✅ 响应消息:', response.data.message)
|
||
console.log('✅ 原始评论数据:', response.data.result)
|
||
console.log('✅ 评论数据数量:', response.data.result.length || 0)
|
||
|
||
// 适配数据格式
|
||
const adaptedComments: CourseComment[] = response.data.result.map((comment: BackendComment) => ({
|
||
id: comment.id,
|
||
userId: comment.userId,
|
||
userName: comment.userName || '匿名用户',
|
||
userAvatar: comment.userAvatar || '',
|
||
userTag: comment.userTag || '',
|
||
content: comment.content || '',
|
||
images: comment.imgs ? comment.imgs.split(',').filter(img => img.trim()) : [], // 图片URL逗号分隔
|
||
isTop: comment.izTop === 1, // 1=置顶,0=普通
|
||
likeCount: comment.likeCount || 0,
|
||
createTime: comment.createTime || '',
|
||
timeAgo: this.formatTimeAgo(comment.createTime) // 计算相对时间
|
||
}))
|
||
|
||
console.log('✅ 适配后的评论数据:', adaptedComments)
|
||
|
||
return {
|
||
code: response.data.code,
|
||
message: response.data.message,
|
||
data: adaptedComments
|
||
}
|
||
} else {
|
||
console.warn('⚠️ API返回的数据结构不正确:', response.data)
|
||
return {
|
||
code: 500,
|
||
message: '数据格式错误',
|
||
data: []
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 评论API调用失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 格式化时间为相对时间显示
|
||
private static formatTimeAgo(createTime: string): string {
|
||
if (!createTime) return '未知时间'
|
||
|
||
try {
|
||
const now = new Date()
|
||
const commentTime = new Date(createTime)
|
||
const diffMs = now.getTime() - commentTime.getTime()
|
||
|
||
const diffMinutes = Math.floor(diffMs / (1000 * 60))
|
||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||
const diffWeeks = Math.floor(diffDays / 7)
|
||
const diffMonths = Math.floor(diffDays / 30)
|
||
|
||
if (diffMinutes < 1) return '刚刚'
|
||
if (diffMinutes < 60) return `${diffMinutes}分钟前`
|
||
if (diffHours < 24) return `${diffHours}小时前`
|
||
if (diffDays < 7) return `${diffDays}天前`
|
||
if (diffWeeks < 4) return `${diffWeeks}周前`
|
||
if (diffMonths < 12) return `${diffMonths}个月前`
|
||
|
||
return commentTime.toLocaleDateString()
|
||
} catch (error) {
|
||
console.warn('时间格式化失败:', error)
|
||
return createTime
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
export default CourseApi
|