Merge branch 'dev' of http://110.42.96.65:19890/GoCo/OL-LearnPlatform-Frontend into dev
This commit is contained in:
commit
94ee525ce6
2
.env
2
.env
@ -1,5 +1,5 @@
|
||||
# API配置
|
||||
VITE_API_BASE_URL=http://110.42.96.65:55510/api
|
||||
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
|
||||
|
||||
# Mock配置 - 禁用Mock,使用真实API
|
||||
VITE_ENABLE_MOCK=false
|
||||
|
@ -1,7 +1,7 @@
|
||||
# 开发环境配置
|
||||
|
||||
# API配置
|
||||
VITE_API_BASE_URL=http://110.42.96.65:55510/api
|
||||
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
|
||||
|
||||
# Mock配置
|
||||
# 设置为 true 使用Mock数据,false 使用真实API
|
||||
|
@ -1,7 +1,7 @@
|
||||
# 生产环境配置
|
||||
|
||||
# API配置
|
||||
VITE_API_BASE_URL=http://110.42.96.65:55510/api
|
||||
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
|
||||
|
||||
# Mock配置 - 生产环境禁用Mock,使用真实API
|
||||
VITE_ENABLE_MOCK=false
|
||||
|
@ -73,15 +73,14 @@ export const getCoursesExample = async () => {
|
||||
const response = await CourseApi.getCourses({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
categoryId: 1,
|
||||
difficulty: 1,
|
||||
sortBy: 'createdAt'
|
||||
categoryId: '1',
|
||||
difficulty: '1'
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
const { list, total } = response.data
|
||||
console.log('课程列表:', list)
|
||||
console.log('总数:', total)
|
||||
const courses = response.data
|
||||
console.log('课程列表:', courses)
|
||||
console.log('总数:', courses.length)
|
||||
return response.data
|
||||
}
|
||||
} catch (error) {
|
||||
@ -113,7 +112,7 @@ export const searchCoursesExample = async () => {
|
||||
}
|
||||
|
||||
// 获取课程详情示例
|
||||
export const getCourseDetailExample = async (courseId: number) => {
|
||||
export const getCourseDetailExample = async (courseId: string) => {
|
||||
try {
|
||||
const response = await CourseApi.getCourseById(courseId)
|
||||
if (response.code === 200) {
|
||||
|
@ -12,13 +12,13 @@ export { default as UploadApi } from './modules/upload'
|
||||
export { default as StatisticsApi } from './modules/statistics'
|
||||
|
||||
// API 基础配置
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api'
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot'
|
||||
|
||||
// API 端点配置
|
||||
export const API_ENDPOINTS = {
|
||||
// 认证相关
|
||||
AUTH: {
|
||||
LOGIN: '/auth/login',
|
||||
LOGIN: '/biz/user/login',
|
||||
REGISTER: '/auth/register',
|
||||
LOGOUT: '/auth/logout',
|
||||
REFRESH: '/auth/refresh',
|
||||
@ -34,12 +34,12 @@ export const API_ENDPOINTS = {
|
||||
|
||||
// 课程相关
|
||||
COURSES: {
|
||||
LIST: '/courses',
|
||||
LIST: '/biz/course/list',
|
||||
SEARCH: '/courses/search',
|
||||
POPULAR: '/courses/popular',
|
||||
LATEST: '/courses/latest',
|
||||
RECOMMENDED: '/courses/recommended',
|
||||
DETAIL: '/courses/:id',
|
||||
DETAIL: '/biz/course/detail',
|
||||
CHAPTERS: '/courses/:id/chapters',
|
||||
LESSONS: '/courses/:id/lessons',
|
||||
ENROLL: '/courses/:id/enroll',
|
||||
@ -52,9 +52,19 @@ export const API_ENDPOINTS = {
|
||||
|
||||
// 分类相关
|
||||
CATEGORIES: {
|
||||
LIST: '/categories',
|
||||
LIST: '/biz/course/category/list',
|
||||
COURSES: '/categories/:id/courses',
|
||||
},
|
||||
|
||||
// 专题相关
|
||||
SUBJECTS: {
|
||||
LIST: '/biz/course/subject/list',
|
||||
},
|
||||
|
||||
// 难度相关
|
||||
DIFFICULTIES: {
|
||||
LIST: '/biz/course/difficulty/list',
|
||||
},
|
||||
|
||||
// 章节课时相关
|
||||
CHAPTERS: {
|
||||
|
@ -17,10 +17,16 @@ export class AuthApi {
|
||||
// 用户登录
|
||||
static async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
try {
|
||||
console.log('🚀 发送登录请求:', { url: '/users/login', data: { ...data, password: '***' } })
|
||||
// 转换参数格式:将email/phone转换为username
|
||||
const loginData = {
|
||||
username: data.email || data.phone || '',
|
||||
password: data.password
|
||||
}
|
||||
|
||||
console.log('🚀 发送登录请求:', { url: '/biz/user/login', data: { ...loginData, password: '***' } })
|
||||
|
||||
// 调用后端API
|
||||
const response = await ApiRequest.post<any>('/users/login', data)
|
||||
const response = await ApiRequest.post<any>('/biz/user/login', loginData)
|
||||
|
||||
console.log('🔍 Login API Response:', response)
|
||||
console.log('🔍 Response Code:', response.code)
|
||||
@ -34,12 +40,19 @@ export class AuthApi {
|
||||
|
||||
// 如果response.code是undefined,检查response.data是否包含完整的API响应
|
||||
if (actualCode === undefined && actualData && typeof actualData === 'object') {
|
||||
if ('code' in actualData && 'message' in actualData && 'data' in actualData) {
|
||||
// 这种情况下,真正的API响应被包装在了response.data中
|
||||
// 检查是否是标准的jeecg-boot响应格式 (success, code, message, result)
|
||||
if ('success' in actualData && 'code' in actualData && 'message' in actualData && 'result' in actualData) {
|
||||
actualCode = actualData.code
|
||||
actualMessage = actualData.message
|
||||
actualData = actualData.result
|
||||
console.log('🔧 修正后的响应 (jeecg-boot格式):', { code: actualCode, message: actualMessage, data: actualData })
|
||||
}
|
||||
// 检查是否是其他格式 (code, message, data)
|
||||
else if ('code' in actualData && 'message' in actualData && 'data' in actualData) {
|
||||
actualCode = actualData.code
|
||||
actualMessage = actualData.message
|
||||
actualData = actualData.data
|
||||
console.log('🔧 修正后的响应:', { code: actualCode, message: actualMessage, data: actualData })
|
||||
console.log('🔧 修正后的响应 (标准格式):', { code: actualCode, message: actualMessage, data: actualData })
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,8 +67,8 @@ export class AuthApi {
|
||||
} as ApiResponse<LoginResponse>
|
||||
}
|
||||
|
||||
// 如果后端返回的是真实API格式(包含token, timestamp, expires)
|
||||
if (actualData && actualData.token && actualData.timestamp) {
|
||||
// 如果后端返回的是jeecg-boot格式(只包含token)
|
||||
if (actualData && actualData.token) {
|
||||
const adaptedResponse: ApiResponse<LoginResponse> = {
|
||||
code: actualCode,
|
||||
message: actualMessage || '登录成功',
|
||||
@ -64,7 +77,7 @@ export class AuthApi {
|
||||
id: 1, // 真实API没有返回用户ID,使用默认值
|
||||
email: data.email || '',
|
||||
phone: data.phone || '',
|
||||
username: data.phone || data.email?.split('@')[0] || 'user',
|
||||
username: data.email || data.phone || '',
|
||||
nickname: '用户',
|
||||
avatar: '',
|
||||
role: 'student',
|
||||
@ -216,7 +229,7 @@ export class AuthApi {
|
||||
|
||||
// 获取当前用户信息
|
||||
static getCurrentUser(): Promise<ApiResponse<User>> {
|
||||
return ApiRequest.get('/auth/me')
|
||||
return ApiRequest.get('/users/info')
|
||||
}
|
||||
|
||||
// 更新用户资料
|
||||
|
@ -5,20 +5,24 @@ import type {
|
||||
PaginationResponse,
|
||||
Course,
|
||||
CourseCategory,
|
||||
CourseSubject,
|
||||
CourseDifficulty,
|
||||
CourseListQueryParams,
|
||||
BackendCourseItem,
|
||||
Chapter,
|
||||
Lesson,
|
||||
LessonResource,
|
||||
CourseSection,
|
||||
CourseSectionListResponse,
|
||||
BackendCourseSection,
|
||||
BackendCourseSectionListResponse,
|
||||
BackendInstructor,
|
||||
BackendSectionVideo,
|
||||
SectionVideo,
|
||||
VideoQuality,
|
||||
Quiz,
|
||||
LearningProgress,
|
||||
SearchRequest,
|
||||
Instructor,
|
||||
BackendCourse,
|
||||
BackendCourseListResponse,
|
||||
CourseListRequest,
|
||||
} from '../types'
|
||||
|
||||
/**
|
||||
@ -62,142 +66,97 @@ export class CourseApi {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 计算课程时长
|
||||
* 映射后端难度值到前端级别
|
||||
*/
|
||||
private static calculateDuration(startTime: string, endTime: string): string {
|
||||
try {
|
||||
const start = new Date(startTime)
|
||||
const end = new Date(endTime)
|
||||
const diffMs = end.getTime() - start.getTime()
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24))
|
||||
return `${diffDays}天`
|
||||
} catch (error) {
|
||||
return '待定'
|
||||
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?: CourseListRequest): Promise<ApiResponse<PaginationResponse<Course>>> {
|
||||
static async getCourses(params?: CourseListQueryParams): Promise<ApiResponse<Course[]>> {
|
||||
try {
|
||||
console.log('调用课程列表API,参数:', params)
|
||||
console.log('🚀 调用课程列表API,参数:', params)
|
||||
|
||||
// 构建查询参数,根据API文档的参数名称
|
||||
const queryParams: any = {}
|
||||
if (params?.categoryId) queryParams.categoryId = params.categoryId
|
||||
if (params?.difficulty !== undefined) queryParams.difficulty = params.difficulty
|
||||
if (params?.difficulty) queryParams.difficulty = params.difficulty
|
||||
if (params?.subject) queryParams.subject = params.subject
|
||||
if (params?.page) queryParams.page = params.page
|
||||
if (params?.pageSize) queryParams.pageSize = params.pageSize
|
||||
if (params?.keyword) queryParams.keyword = params.keyword
|
||||
|
||||
console.log('🔍 查询参数:', queryParams)
|
||||
|
||||
// 调用后端API
|
||||
const response = await ApiRequest.get<BackendCourseListResponse>('/lesson/list', queryParams)
|
||||
console.log('课程列表API响应:', response)
|
||||
console.log('响应数据结构:', response.data)
|
||||
console.log('响应数据类型:', typeof response.data)
|
||||
const response = await ApiRequest.get<any>('/biz/course/list', queryParams)
|
||||
console.log('🔍 课程列表API响应:', response)
|
||||
|
||||
// 检查是否是axios响应格式还是我们的ApiResponse格式
|
||||
let actualData: any
|
||||
let actualCode: number
|
||||
let actualMessage: string
|
||||
// 处理后端响应格式
|
||||
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 || '未知讲师',
|
||||
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 || ''
|
||||
}))
|
||||
|
||||
// 使用类型断言来处理不同的响应格式
|
||||
const responseAny = response as any
|
||||
|
||||
if (responseAny.data && typeof responseAny.data === 'object' && 'data' in responseAny.data) {
|
||||
// 这是我们期望的ApiResponse格式: { code, message, data: { list, total } }
|
||||
actualData = responseAny.data.data
|
||||
actualCode = responseAny.data.code
|
||||
actualMessage = responseAny.data.message
|
||||
console.log('检测到ApiResponse格式')
|
||||
} else {
|
||||
// 这可能是直接的axios响应格式: { list, total }
|
||||
actualData = responseAny.data
|
||||
actualCode = responseAny.status || 200
|
||||
actualMessage = responseAny.statusText || 'OK'
|
||||
console.log('检测到直接响应格式')
|
||||
}
|
||||
|
||||
console.log('实际数据:', actualData)
|
||||
console.log('实际数据的list字段:', actualData?.list)
|
||||
console.log('list是否为数组:', Array.isArray(actualData?.list))
|
||||
|
||||
// 检查响应数据的有效性
|
||||
if (!actualData || !Array.isArray(actualData.list)) {
|
||||
console.warn('API响应数据格式异常')
|
||||
console.warn('期望的格式: { list: [], total: number }')
|
||||
console.warn('实际收到的格式:', actualData)
|
||||
return {
|
||||
code: actualCode || 0,
|
||||
message: actualMessage || '数据格式异常',
|
||||
data: {
|
||||
list: [],
|
||||
total: 0,
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || 10,
|
||||
totalPages: 0
|
||||
}
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: courses
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 课程列表API返回格式异常:', response)
|
||||
return {
|
||||
code: 500,
|
||||
message: response.data?.message || '获取课程列表失败',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
|
||||
// 适配后端响应格式为前端期望的格式
|
||||
const adaptedCourses: Course[] = actualData.list.map((backendCourse: BackendCourse) => ({
|
||||
id: backendCourse.id,
|
||||
title: backendCourse.name,
|
||||
description: backendCourse.description,
|
||||
thumbnail: backendCourse.cover,
|
||||
coverImage: backendCourse.cover,
|
||||
price: parseFloat(backendCourse.price || '0'),
|
||||
originalPrice: parseFloat(backendCourse.price || '0'),
|
||||
currency: 'CNY',
|
||||
rating: 4.5, // 默认评分,后端没有返回
|
||||
ratingCount: 0, // 默认评分数量
|
||||
studentsCount: 0, // 默认学生数量
|
||||
duration: '待定', // 默认时长
|
||||
totalLessons: 0, // 默认课程数量
|
||||
level: this.mapDifficulty(backendCourse.difficulty || 0),
|
||||
language: 'zh-CN',
|
||||
category: {
|
||||
id: backendCourse.categoryId,
|
||||
name: this.getCategoryName(backendCourse.categoryId),
|
||||
slug: 'category-' + backendCourse.categoryId
|
||||
},
|
||||
tags: backendCourse.subject ? [backendCourse.subject] : [],
|
||||
skills: [],
|
||||
requirements: backendCourse.prerequisite ? [backendCourse.prerequisite] : [],
|
||||
objectives: backendCourse.target ? [backendCourse.target] : [],
|
||||
instructor: {
|
||||
id: backendCourse.teacherId || 0,
|
||||
name: backendCourse.school || '未知讲师',
|
||||
title: '讲师',
|
||||
bio: '',
|
||||
avatar: '',
|
||||
rating: 4.5,
|
||||
studentsCount: 0,
|
||||
coursesCount: 0,
|
||||
experience: '',
|
||||
education: [],
|
||||
certifications: []
|
||||
},
|
||||
status: 'published' as const,
|
||||
createdAt: this.formatTimestamp(backendCourse.createdTime),
|
||||
updatedAt: this.formatTimestamp(backendCourse.updatedTime),
|
||||
publishedAt: this.formatTimestamp(backendCourse.createdTime)
|
||||
}))
|
||||
|
||||
const adaptedResponse: ApiResponse<PaginationResponse<Course>> = {
|
||||
code: actualCode,
|
||||
message: actualMessage,
|
||||
data: {
|
||||
list: adaptedCourses,
|
||||
total: actualData.total || 0,
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || 10,
|
||||
totalPages: Math.ceil((actualData.total || 0) / (params?.pageSize || 10))
|
||||
}
|
||||
}
|
||||
|
||||
return adaptedResponse
|
||||
} catch (error: any) {
|
||||
console.error('课程API调用失败:', error)
|
||||
|
||||
@ -214,13 +173,96 @@ export class CourseApi {
|
||||
return {
|
||||
code: 500,
|
||||
message: errorMessage,
|
||||
data: {
|
||||
list: [],
|
||||
total: 0,
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || 10,
|
||||
totalPages: 0
|
||||
data: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取课程详情 - 适配后端接口
|
||||
static async getCourseDetail(id: string): Promise<ApiResponse<Course>> {
|
||||
try {
|
||||
console.log('🚀 调用课程详情API,课程ID:', id)
|
||||
|
||||
// 调用后端API
|
||||
const response = await ApiRequest.get<any>('/biz/course/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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -246,86 +288,89 @@ export class CourseApi {
|
||||
}
|
||||
|
||||
// 获取课程详情 - 适配后端接口
|
||||
static async getCourseById(id: number): Promise<ApiResponse<Course>> {
|
||||
static async getCourseById(id: string): Promise<ApiResponse<Course>> {
|
||||
try {
|
||||
// 调用后端课程详情接口
|
||||
const response = await ApiRequest.get<BackendCourse>('/lesson/detail', { id })
|
||||
const response = await ApiRequest.get<any>('/biz/course/detail', { id })
|
||||
|
||||
// 检查是否是axios响应格式还是我们的ApiResponse格式
|
||||
let actualData: any
|
||||
let actualCode: number
|
||||
let actualMessage: string
|
||||
console.log('🔍 课程详情API响应:', response)
|
||||
|
||||
// 使用类型断言来处理不同的响应格式
|
||||
const responseAny = response as any
|
||||
// 处理后端响应格式
|
||||
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)
|
||||
}
|
||||
|
||||
if (responseAny.data && typeof responseAny.data === 'object' && 'data' in responseAny.data) {
|
||||
// 这是我们期望的ApiResponse格式: { code, message, data: BackendCourse }
|
||||
actualData = responseAny.data.data
|
||||
actualCode = responseAny.data.code
|
||||
actualMessage = responseAny.data.message
|
||||
console.log('检测到ApiResponse格式')
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: course
|
||||
}
|
||||
} else {
|
||||
// 这可能是直接的axios响应格式: BackendCourse
|
||||
actualData = responseAny.data
|
||||
actualCode = responseAny.status || 200
|
||||
actualMessage = responseAny.statusText || 'OK'
|
||||
console.log('检测到直接响应格式')
|
||||
console.warn('⚠️ 课程详情API返回格式异常:', response)
|
||||
return {
|
||||
code: 500,
|
||||
message: response.data?.message || '获取课程详情失败',
|
||||
data: {} as Course
|
||||
}
|
||||
}
|
||||
|
||||
// 适配数据格式
|
||||
const adaptedCourse: Course = {
|
||||
id: actualData.id,
|
||||
title: actualData.name,
|
||||
description: actualData.description,
|
||||
content: actualData.outline, // 使用 outline 作为课程内容
|
||||
thumbnail: actualData.cover,
|
||||
coverImage: actualData.cover,
|
||||
price: parseFloat(actualData.price || '0'),
|
||||
originalPrice: parseFloat(actualData.price || '0'),
|
||||
currency: 'CNY',
|
||||
rating: 4.5,
|
||||
ratingCount: 0,
|
||||
studentsCount: 0,
|
||||
duration: this.calculateDuration(actualData.startTime, actualData.endTime),
|
||||
totalLessons: 0,
|
||||
level: 'beginner' as const,
|
||||
language: 'zh-CN',
|
||||
category: {
|
||||
id: actualData.categoryId,
|
||||
name: '未分类',
|
||||
slug: 'uncategorized'
|
||||
},
|
||||
tags: [],
|
||||
skills: [],
|
||||
requirements: actualData.prerequisite ? [actualData.prerequisite] : [],
|
||||
objectives: actualData.target ? [actualData.target] : [],
|
||||
instructor: {
|
||||
id: actualData.teacherId || 0,
|
||||
name: actualData.school || '未知讲师',
|
||||
title: '讲师',
|
||||
bio: actualData.position || '',
|
||||
avatar: '',
|
||||
rating: 4.5,
|
||||
studentsCount: 0,
|
||||
coursesCount: 0,
|
||||
experience: actualData.arrangement || '',
|
||||
education: [],
|
||||
certifications: []
|
||||
},
|
||||
status: 'published' as const,
|
||||
createdAt: this.formatTimestamp(actualData.createdTime),
|
||||
updatedAt: this.formatTimestamp(actualData.updatedTime),
|
||||
publishedAt: actualData.startTime
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 获取课程详情失败:', error)
|
||||
return {
|
||||
code: actualCode,
|
||||
message: actualMessage,
|
||||
data: adaptedCourse
|
||||
code: 500,
|
||||
message: error.message || '获取课程详情失败',
|
||||
data: {} as Course
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,8 +400,135 @@ export class CourseApi {
|
||||
}
|
||||
|
||||
// 获取课程分类
|
||||
static getCategories(): Promise<ApiResponse<CourseCategory[]>> {
|
||||
return ApiRequest.get('/categories')
|
||||
static async getCategories(): Promise<ApiResponse<CourseCategory[]>> {
|
||||
try {
|
||||
console.log('🚀 获取课程分类列表')
|
||||
|
||||
// 调用后端API(不需要token)
|
||||
const response = await ApiRequest.get<any>('/biz/course/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>('/biz/course/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>('/biz/course/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: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类下的课程
|
||||
@ -382,94 +554,65 @@ export class CourseApi {
|
||||
}
|
||||
|
||||
// 获取课程章节列表
|
||||
static async getCourseSections(lessonId: number): Promise<ApiResponse<CourseSectionListResponse>> {
|
||||
static async getCourseSections(courseId: string): Promise<ApiResponse<CourseSectionListResponse>> {
|
||||
try {
|
||||
console.log('尝试从API获取课程章节数据,课程ID:', lessonId)
|
||||
console.log('API请求URL: /lesson/section/list')
|
||||
console.log('API请求参数:', { lesson_id: lessonId.toString() })
|
||||
console.log('🔍 获取课程章节数据,课程ID:', courseId)
|
||||
console.log('🔍 API请求URL: /biz/course/' + courseId + '/section')
|
||||
|
||||
const backendResponse = await ApiRequest.get<BackendCourseSectionListResponse>('/lesson/section/list', { lesson_id: lessonId.toString() })
|
||||
console.log('章节API响应:', backendResponse)
|
||||
const response = await ApiRequest.get<any>(`/biz/course/${courseId}/section`)
|
||||
console.log('🔍 章节API响应:', response)
|
||||
|
||||
// 检查是否是axios响应格式还是我们的ApiResponse格式
|
||||
let actualData: any
|
||||
let actualCode: number
|
||||
let actualMessage: string
|
||||
let actualTimestamp: string | undefined
|
||||
// 处理后端响应格式
|
||||
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 responseAny = backendResponse as any
|
||||
// 适配数据格式
|
||||
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
|
||||
}))
|
||||
|
||||
if (responseAny.data && typeof responseAny.data === 'object' && 'data' in responseAny.data) {
|
||||
// 这是我们期望的ApiResponse格式: { code, message, data: { list }, timestamp }
|
||||
actualData = responseAny.data.data
|
||||
actualCode = responseAny.data.code
|
||||
actualMessage = responseAny.data.message
|
||||
actualTimestamp = responseAny.data.timestamp?.toString()
|
||||
console.log('检测到ApiResponse格式')
|
||||
} else {
|
||||
// 这可能是直接的axios响应格式: { list }
|
||||
actualData = responseAny.data
|
||||
actualCode = responseAny.status || 200
|
||||
actualMessage = responseAny.statusText || 'OK'
|
||||
actualTimestamp = undefined
|
||||
console.log('检测到直接响应格式')
|
||||
}
|
||||
console.log('✅ 适配后的章节数据:', adaptedSections)
|
||||
|
||||
console.log('响应状态码:', actualCode)
|
||||
console.log('响应消息:', actualMessage)
|
||||
console.log('原始章节数据:', actualData?.list)
|
||||
console.log('章节数据数量:', actualData?.list?.length || 0)
|
||||
|
||||
// 检查数据是否存在
|
||||
if (!actualData || !Array.isArray(actualData.list)) {
|
||||
console.warn('API返回的数据结构不正确:', actualData)
|
||||
return {
|
||||
code: actualCode,
|
||||
message: actualMessage,
|
||||
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: actualTimestamp || ''
|
||||
},
|
||||
timestamp: actualTimestamp
|
||||
traceId: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配数据格式
|
||||
const adaptedSections: CourseSection[] = actualData.list.map((section: BackendCourseSection) => ({
|
||||
id: section.id,
|
||||
lessonId: section.lessonId,
|
||||
outline: section.videoUrl, // 将videoUrl映射到outline
|
||||
name: section.name,
|
||||
parentId: section.parentId,
|
||||
sort: section.sortOrder, // 将sortOrder映射到sort
|
||||
level: section.level === 0 ? 1 : 0, // 转换level逻辑:API中0=子级,1=父级;前端中0=父级,1=子级
|
||||
revision: section.revision,
|
||||
createdAt: section.createdTime ? new Date(section.createdTime).getTime() : null,
|
||||
updatedAt: section.updatedTime ? new Date(section.updatedTime).getTime() : null,
|
||||
deletedAt: null,
|
||||
completed: false,
|
||||
duration: undefined
|
||||
}))
|
||||
|
||||
console.log('适配后的章节数据:', adaptedSections)
|
||||
|
||||
const adaptedResponse: ApiResponse<CourseSectionListResponse> = {
|
||||
code: actualCode,
|
||||
message: actualMessage,
|
||||
data: {
|
||||
list: adaptedSections,
|
||||
timestamp: Date.now(),
|
||||
traceId: actualTimestamp || ''
|
||||
},
|
||||
timestamp: actualTimestamp
|
||||
}
|
||||
|
||||
return adaptedResponse
|
||||
} catch (error) {
|
||||
console.error('章节API调用失败:', error)
|
||||
console.error('错误详情:', {
|
||||
console.error('❌ 章节API调用失败:', error)
|
||||
console.error('❌ 错误详情:', {
|
||||
message: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
response: (error as any).response?.data,
|
||||
@ -622,33 +765,189 @@ export class CourseApi {
|
||||
return ApiRequest.get(`/courses/${courseId}/access`)
|
||||
}
|
||||
|
||||
// 辅助方法:映射难度等级
|
||||
private static mapDifficulty(difficulty: number): 'beginner' | 'intermediate' | 'advanced' {
|
||||
switch (difficulty) {
|
||||
case 0:
|
||||
return 'beginner'
|
||||
case 1:
|
||||
return 'intermediate'
|
||||
case 2:
|
||||
return 'advanced'
|
||||
default:
|
||||
return 'beginner'
|
||||
// 获取课程讲师列表
|
||||
static async getCourseInstructors(courseId: string): Promise<ApiResponse<Instructor[]>> {
|
||||
try {
|
||||
console.log('🔍 获取课程讲师数据,课程ID:', courseId)
|
||||
console.log('🔍 API请求URL: /biz/course/' + courseId + '/teachers')
|
||||
|
||||
const response = await ApiRequest.get<any>(`/biz/course/${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
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法:获取分类名称
|
||||
private static getCategoryName(categoryId: number): string {
|
||||
// 这里可以根据categoryId返回对应的分类名称
|
||||
// 暂时返回默认值,后续可以通过分类API获取
|
||||
const categoryMap: { [key: number]: string } = {
|
||||
1: '信息技术',
|
||||
2: '数学',
|
||||
3: '物理',
|
||||
4: '化学',
|
||||
5: '生物'
|
||||
// 获取章节视频列表
|
||||
static async getSectionVideos(courseId: string, sectionId: string): Promise<ApiResponse<SectionVideo[]>> {
|
||||
try {
|
||||
console.log('🔍 获取章节视频数据,课程ID:', courseId, '章节ID:', sectionId)
|
||||
console.log('🔍 API请求URL: /biz/course/' + courseId + '/section_video/' + sectionId)
|
||||
|
||||
const response = await ApiRequest.get<any>(`/biz/course/${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
|
||||
}
|
||||
return categoryMap[categoryId] || '其他'
|
||||
}
|
||||
|
||||
// 解析视频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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CourseApi
|
||||
|
@ -12,9 +12,6 @@ const checkNetworkStatus = (): boolean => {
|
||||
return true // 默认认为网络可用
|
||||
}
|
||||
|
||||
// 全局开关:强制使用Mock数据(当前需求:全部使用Mock,不访问后端)
|
||||
const FORCE_ENABLE_MOCK = true
|
||||
|
||||
// 消息提示函数 - 使用window.alert作为fallback,实际项目中应该使用UI库的消息组件
|
||||
const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
|
||||
// 这里可以替换为你使用的UI库的消息组件
|
||||
@ -30,21 +27,36 @@ const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'i
|
||||
|
||||
// 创建axios实例
|
||||
const request: AxiosInstance = axios.create({
|
||||
// 统一走 /api,由 Vite 代理到后端,避免CORS;若启用Mock,则只会走本地Mock
|
||||
baseURL: '/api',
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/jeecgboot',
|
||||
timeout: 30000, // 增加到30秒
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// 不需要token的接口列表
|
||||
const NO_TOKEN_URLS = [
|
||||
'/biz/course/category/list',
|
||||
'/biz/course/subject/list',
|
||||
'/biz/course/difficulty/list',
|
||||
'/biz/course/list',
|
||||
'/biz/course/detail',
|
||||
// 可以在这里添加其他不需要token的接口
|
||||
]
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 添加认证token
|
||||
const userStore = useUserStore()
|
||||
if (userStore.token) {
|
||||
config.headers.Authorization = `Bearer ${userStore.token}`
|
||||
// 检查是否需要添加token
|
||||
const needToken = !NO_TOKEN_URLS.some(url => config.url?.includes(url))
|
||||
|
||||
if (needToken) {
|
||||
// 添加认证token(直接传token,不需要Bearer前缀)
|
||||
const userStore = useUserStore()
|
||||
const token = userStore.token || localStorage.getItem('X-Access-Token') || ''
|
||||
if (token) {
|
||||
config.headers['X-Access-Token'] = token
|
||||
}
|
||||
}
|
||||
|
||||
// 添加请求时间戳
|
||||
@ -180,242 +192,7 @@ request.interceptors.response.use(
|
||||
}
|
||||
)
|
||||
|
||||
// 导入Mock数据
|
||||
import { mockCourses, getMockCourseSections } from './mock/courses'
|
||||
// getMockCourseDetail 暂时注释,后续需要时再启用
|
||||
|
||||
// Mock数据处理
|
||||
const handleMockRequest = async <T = any>(url: string, method: string, data?: any): Promise<ApiResponse<T>> => {
|
||||
console.log('🚀 Mock Request:', { url, method, data })
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 登录Mock
|
||||
if (url === '/users/login' && method === 'POST') {
|
||||
const { email, phone, password } = data || {}
|
||||
const loginField = phone || email
|
||||
|
||||
// 模拟登录验证
|
||||
if (loginField && password) {
|
||||
return {
|
||||
code: 200,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
user: {
|
||||
id: 1,
|
||||
email: email || `${phone}@example.com`,
|
||||
phone: phone || '123456789',
|
||||
username: phone || email?.split('@')[0] || 'user',
|
||||
nickname: '测试用户',
|
||||
avatar: 'https://via.placeholder.com/100',
|
||||
role: 'student',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z'
|
||||
},
|
||||
token: 'mock_jwt_token_' + Date.now(),
|
||||
refreshToken: 'mock_refresh_token_' + Date.now(),
|
||||
expiresIn: 3600
|
||||
}
|
||||
} as ApiResponse<T>
|
||||
} else {
|
||||
return {
|
||||
code: 400,
|
||||
message: '手机号/邮箱或密码不能为空',
|
||||
data: null
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
}
|
||||
|
||||
// 注册Mock
|
||||
if (url === '/auth/register' && method === 'POST') {
|
||||
const { email, password, verificationCode } = data || {}
|
||||
|
||||
if (!email || !password) {
|
||||
return {
|
||||
code: 400,
|
||||
message: '邮箱和密码不能为空',
|
||||
data: null
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
if (!verificationCode) {
|
||||
return {
|
||||
code: 400,
|
||||
message: '验证码不能为空',
|
||||
data: null
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '注册成功',
|
||||
data: {
|
||||
id: 2,
|
||||
email: email,
|
||||
username: email.split('@')[0],
|
||||
nickname: '新用户',
|
||||
avatar: '',
|
||||
role: 'student',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 发送验证码Mock
|
||||
if (url === '/auth/send-verification' && method === 'POST') {
|
||||
return {
|
||||
code: 200,
|
||||
message: '验证码已发送',
|
||||
data: null
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 获取当前用户信息Mock
|
||||
if (url === '/auth/me' && method === 'GET') {
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
id: 1,
|
||||
email: 'test@example.com',
|
||||
username: 'test',
|
||||
nickname: '测试用户',
|
||||
avatar: '',
|
||||
role: 'student',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
updatedAt: '2024-01-01T00:00:00Z'
|
||||
}
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 用户登出Mock
|
||||
if (url === '/auth/logout' && method === 'POST') {
|
||||
return {
|
||||
code: 200,
|
||||
message: '登出成功',
|
||||
data: null
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 课程详情Mock
|
||||
if (url === '/lesson/detail' && method === 'GET') {
|
||||
// 对于GET请求,参数直接在data中(data就是params对象)
|
||||
const id = data?.id
|
||||
console.log('课程详情Mock - 获取到的ID:', id, '原始data:', data)
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
code: 400,
|
||||
message: '课程ID必填',
|
||||
data: null
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 根据课程ID提供不同的模拟数据
|
||||
const courseData = {
|
||||
1: {
|
||||
name: 'DeepSeek大语言模型实战应用',
|
||||
cover: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=300&fit=crop',
|
||||
price: '299.00',
|
||||
school: 'DeepSeek技术学院',
|
||||
description: '本课程深度聚焦DeepSeek大语言模型的实际应用,让每一位学员了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。',
|
||||
position: 'AI技术专家 / 高级讲师'
|
||||
},
|
||||
2: {
|
||||
name: 'Python编程基础与实战',
|
||||
cover: 'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?w=400&h=300&fit=crop',
|
||||
price: '199.00',
|
||||
school: '编程技术学院',
|
||||
description: '从零开始学习Python编程,涵盖基础语法、数据结构、面向对象编程等核心概念,通过实际项目练习掌握Python开发技能。',
|
||||
position: 'Python开发专家 / 资深讲师'
|
||||
},
|
||||
3: {
|
||||
name: 'Web前端开发全栈课程',
|
||||
cover: 'https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=400&h=300&fit=crop',
|
||||
price: '399.00',
|
||||
school: '前端技术学院',
|
||||
description: '全面学习现代Web前端开发技术,包括HTML5、CSS3、JavaScript、Vue.js、React等主流框架,培养全栈开发能力。',
|
||||
position: '前端架构师 / 技术总监'
|
||||
}
|
||||
}
|
||||
|
||||
const currentCourse = courseData[id as keyof typeof courseData] || courseData[1]
|
||||
|
||||
// 模拟课程详情数据
|
||||
return {
|
||||
code: 0,
|
||||
message: '查询课程详情成功',
|
||||
data: {
|
||||
id: parseInt(id),
|
||||
name: currentCourse.name,
|
||||
cover: currentCourse.cover,
|
||||
categoryId: 1,
|
||||
price: currentCourse.price,
|
||||
school: currentCourse.school,
|
||||
description: currentCourse.description,
|
||||
teacherId: 1,
|
||||
outline: '<div><h4>课程大纲:</h4><ul><li><strong>第一章:基础入门</strong><br/>- 环境搭建与配置<br/>- 基本概念理解<br/>- 实践操作演示</li><li><strong>第二章:核心技能</strong><br/>- 核心功能详解<br/>- 实际应用场景<br/>- 案例分析讲解</li><li><strong>第三章:高级应用</strong><br/>- 进阶技巧掌握<br/>- 项目实战演练<br/>- 问题解决方案</li></ul></div>',
|
||||
prerequisite: '具备基本的计算机操作能力',
|
||||
target: '掌握核心技能,能够在实际工作中熟练应用',
|
||||
arrangement: '理论与实践相结合,循序渐进的学习方式',
|
||||
startTime: '2025-01-26 10:13:17',
|
||||
endTime: '2025-03-26 10:13:17',
|
||||
revision: 1,
|
||||
position: currentCourse.position,
|
||||
createdAt: 1737944724,
|
||||
updatedAt: 1737944724,
|
||||
updatedTime: null
|
||||
}
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 课程列表Mock
|
||||
if (url === '/lesson/list' && method === 'GET') {
|
||||
return {
|
||||
code: 0,
|
||||
message: '查询课程列表成功',
|
||||
data: {
|
||||
list: mockCourses,
|
||||
total: mockCourses.length
|
||||
}
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 课程章节列表Mock
|
||||
if (url === '/lesson/section/list' && method === 'GET') {
|
||||
const lessonId = data?.lesson_id
|
||||
console.log('课程章节Mock - 获取到的lesson_id:', lessonId, '原始data:', data)
|
||||
|
||||
if (!lessonId) {
|
||||
return {
|
||||
code: 400,
|
||||
message: '课程ID必填',
|
||||
data: null
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
const mockSections = getMockCourseSections(parseInt(lessonId))
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: {
|
||||
list: mockSections,
|
||||
total: mockSections.length
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 默认404响应
|
||||
return {
|
||||
code: 404,
|
||||
message: '接口不存在',
|
||||
data: null
|
||||
} as ApiResponse<T>
|
||||
}
|
||||
|
||||
// 重试机制
|
||||
const retryRequest = async <T = any>(
|
||||
@ -459,19 +236,7 @@ export class ApiRequest {
|
||||
params?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<ApiResponse<T>> {
|
||||
const enableMock = FORCE_ENABLE_MOCK || ((import.meta as any).env?.VITE_ENABLE_MOCK === 'true')
|
||||
// 优先:若显式启用Mock,直接使用Mock
|
||||
if (enableMock) {
|
||||
return handleMockRequest<T>(url, 'GET', params)
|
||||
}
|
||||
|
||||
try {
|
||||
return await retryRequest(() => request.get(url, { params, ...config }))
|
||||
} catch (error) {
|
||||
console.warn('API请求失败,降级到Mock数据:', error)
|
||||
// 后备:真实API失败时,仍回落到Mock,保障页面可用
|
||||
return handleMockRequest<T>(url, 'GET', params)
|
||||
}
|
||||
return await retryRequest(() => request.get(url, { params, ...config }))
|
||||
}
|
||||
|
||||
// POST 请求
|
||||
@ -480,17 +245,7 @@ export class ApiRequest {
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<ApiResponse<T>> {
|
||||
const enableMock = FORCE_ENABLE_MOCK || ((import.meta as any).env?.VITE_ENABLE_MOCK === 'true')
|
||||
if (enableMock) {
|
||||
return handleMockRequest<T>(url, 'POST', data)
|
||||
}
|
||||
|
||||
try {
|
||||
return await retryRequest(() => request.post(url, data, config))
|
||||
} catch (error) {
|
||||
console.warn('API请求失败,降级到Mock数据:', error)
|
||||
return handleMockRequest<T>(url, 'POST', data)
|
||||
}
|
||||
return await retryRequest(() => request.post(url, data, config))
|
||||
}
|
||||
|
||||
// PUT 请求
|
||||
|
239
src/api/types.ts
239
src/api/types.ts
@ -50,6 +50,7 @@ export interface UserProfile {
|
||||
export interface LoginRequest {
|
||||
email?: string
|
||||
phone?: string
|
||||
username?: string // 新增username字段,用于适配后端API
|
||||
password: string
|
||||
captcha?: string
|
||||
}
|
||||
@ -81,7 +82,7 @@ export interface RegisterRequest {
|
||||
|
||||
// 课程相关类型
|
||||
export interface Course {
|
||||
id: number
|
||||
id: string // 改为string类型,保持后端ID的原始格式
|
||||
title: string
|
||||
subtitle?: string
|
||||
description: string
|
||||
@ -175,6 +176,134 @@ export interface CourseCategory {
|
||||
children?: CourseCategory[]
|
||||
}
|
||||
|
||||
// 后端分类数据格式
|
||||
export interface BackendCourseCategory {
|
||||
id: string
|
||||
name: string
|
||||
sortOrder: number
|
||||
createBy: string
|
||||
createTime: string
|
||||
updateBy: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
// 后端分类列表响应格式
|
||||
export interface BackendCourseCategoryListResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: BackendCourseCategory[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 后端专题数据格式(字典值格式)
|
||||
export interface BackendCourseSubject {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
// 前端专题数据格式
|
||||
export interface CourseSubject {
|
||||
id: string // 改为string类型,保持后端value字段的原始格式
|
||||
name: string
|
||||
slug: string
|
||||
description?: string
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
// 后端专题列表响应格式
|
||||
export interface BackendCourseSubjectListResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: BackendCourseSubject[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 后端难度数据格式(字典值格式)
|
||||
export interface BackendCourseDifficulty {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
// 前端难度数据格式
|
||||
export interface CourseDifficulty {
|
||||
id: string // 改为string类型,保持后端value字段的原始格式
|
||||
name: string
|
||||
slug: string
|
||||
description?: string
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
// 后端难度列表响应格式
|
||||
export interface BackendCourseDifficultyListResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: BackendCourseDifficulty[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 课程列表查询参数
|
||||
export interface CourseListQueryParams {
|
||||
categoryId?: string // 分类ID
|
||||
difficulty?: string // 难度值
|
||||
subject?: string // 专题值
|
||||
page?: number // 页码
|
||||
pageSize?: number // 每页数量
|
||||
}
|
||||
|
||||
// 后端课程数据格式
|
||||
export interface BackendCourseItem {
|
||||
id: string
|
||||
name: string
|
||||
cover: string
|
||||
video: string
|
||||
school: string
|
||||
description: string
|
||||
type: number
|
||||
target: string
|
||||
difficulty: number
|
||||
subject: string
|
||||
outline: string
|
||||
prerequisite: string
|
||||
reference: string
|
||||
arrangement: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
enrollCount: number
|
||||
maxEnroll: number
|
||||
status: number
|
||||
question: string
|
||||
createBy: string
|
||||
createTime: string
|
||||
updateBy: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
// 后端课程列表响应格式
|
||||
export interface BackendCourseListResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: BackendCourseItem[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 课程详情查询参数
|
||||
export interface CourseDetailQueryParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
// 后端课程详情响应格式
|
||||
export interface BackendCourseDetailResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: BackendCourseItem
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface Instructor {
|
||||
id: number
|
||||
name: string
|
||||
@ -239,32 +368,32 @@ export interface LessonResource {
|
||||
|
||||
// 后端API返回的章节数据结构
|
||||
export interface BackendCourseSection {
|
||||
id: number
|
||||
lessonId: number
|
||||
videoUrl: string // 视频链接
|
||||
id: string
|
||||
courseId: string
|
||||
name: string // 章节名称
|
||||
type: number | null // 章节类型:0=视频、1=资料、2=考试、3=作业,null=未设置
|
||||
sortOrder: number // 排序
|
||||
parentId: number // 父章节ID
|
||||
level: number // 层级:0=子级(课时),1=父级(章节)
|
||||
revision: number // 版本号
|
||||
createdBy: number
|
||||
createdTime: string | null
|
||||
updatedBy: number
|
||||
updatedTime: string | null
|
||||
parentId: string // 父章节ID
|
||||
level: number // 章节层级:0=一级章节、1=二级章节
|
||||
createBy: string
|
||||
createTime: string
|
||||
updateBy: string
|
||||
updateTime: string
|
||||
}
|
||||
|
||||
// 前端使用的课程章节类型(适配后的数据结构)
|
||||
export interface CourseSection {
|
||||
id: number
|
||||
lessonId: number
|
||||
outline: string // 章节大纲/内容链接(从videoUrl适配)
|
||||
id: string // 改为string类型,保持一致性
|
||||
lessonId: string // 改为string类型,与Course.id保持一致
|
||||
outline: string // 章节大纲/内容链接
|
||||
name: string // 章节名称
|
||||
parentId: number // 父章节ID
|
||||
type: number | null // 章节类型:0=视频、1=资料、2=考试、3=作业,null=未设置
|
||||
parentId: string // 父章节ID,改为string类型
|
||||
sort: number // 排序(从sortOrder适配)
|
||||
level: number // 层级:0=父级(章节),1=子级(课时)- 已从后端数据转换
|
||||
level: number // 层级:0=一级章节、1=二级章节
|
||||
revision: number // 版本号
|
||||
createdAt: number | null // 从createdTime适配
|
||||
updatedAt: number | null // 从updatedTime适配
|
||||
createdAt: number | null // 从createTime适配
|
||||
updatedAt: number | null // 从updateTime适配
|
||||
deletedAt: string | null
|
||||
completed?: boolean // 是否已完成(前端状态)
|
||||
duration?: string // 课时时长(前端计算)
|
||||
@ -272,8 +401,78 @@ export interface CourseSection {
|
||||
|
||||
// 后端章节列表响应格式
|
||||
export interface BackendCourseSectionListResponse {
|
||||
list: BackendCourseSection[]
|
||||
total: number
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: BackendCourseSection[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 后端讲师数据结构
|
||||
export interface BackendInstructor {
|
||||
id: string
|
||||
name: string
|
||||
avatar: string
|
||||
title: string
|
||||
tag: string
|
||||
}
|
||||
|
||||
// 后端讲师列表响应格式
|
||||
export interface BackendInstructorListResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: BackendInstructor[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 后端章节视频数据结构
|
||||
export interface BackendSectionVideo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: number
|
||||
fileUrl: string // 视频文件URL,包含多个清晰度
|
||||
thumbnailUrl: string
|
||||
duration: number
|
||||
fileSize: number
|
||||
metadata: string
|
||||
izFeatured: number
|
||||
status: number
|
||||
createdBy: string
|
||||
createdTime: string
|
||||
updatedBy: string
|
||||
updatedTime: string
|
||||
}
|
||||
|
||||
// 后端章节视频列表响应格式
|
||||
export interface BackendSectionVideoListResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
code: number
|
||||
result: BackendSectionVideo[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
// 前端视频质量选项
|
||||
export interface VideoQuality {
|
||||
label: string // 显示名称:1080p, 720p, 480p, 360p
|
||||
value: string // 实际值:1080, 720, 480, 360
|
||||
url: string // 对应的视频URL
|
||||
}
|
||||
|
||||
// 前端章节视频类型
|
||||
export interface SectionVideo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: number
|
||||
thumbnailUrl: string
|
||||
duration: number
|
||||
fileSize: number
|
||||
qualities: VideoQuality[] // 可用的视频质量选项
|
||||
defaultQuality: string // 默认质量(360p)
|
||||
currentQuality: string // 当前选中的质量
|
||||
}
|
||||
|
||||
// 前端章节列表响应格式
|
||||
|
@ -100,15 +100,21 @@ const handleLogin = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
console.log('🚀 开始登录:', { account: loginForm.account, password: '***' })
|
||||
console.log('🔍 表单密码长度:', loginForm.password?.length)
|
||||
console.log('🔍 表单密码内容:', loginForm.password)
|
||||
|
||||
// 判断输入的是手机号还是邮箱
|
||||
const isPhone = /^[0-9]+$/.test(loginForm.account)
|
||||
|
||||
// 调用登录API
|
||||
const response = await AuthApi.login({
|
||||
const loginParams = {
|
||||
...(isPhone ? { phone: loginForm.account } : { email: loginForm.account }),
|
||||
password: loginForm.password
|
||||
})
|
||||
}
|
||||
|
||||
console.log('🔍 准备发送的登录参数:', loginParams)
|
||||
|
||||
// 调用登录API
|
||||
const response = await AuthApi.login(loginParams)
|
||||
|
||||
console.log('✅ 登录响应:', response)
|
||||
|
||||
@ -120,6 +126,7 @@ const handleLogin = async () => {
|
||||
userStore.token = token
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('X-Access-Token', token)
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('refreshToken', refreshToken || '')
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
|
@ -1,661 +0,0 @@
|
||||
// 课程模拟数据
|
||||
import type { Course } from '@/api/types'
|
||||
|
||||
export const mockCourses: Course[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities5.png",
|
||||
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities4.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities3.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course5.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities1.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities5.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities4.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities4.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities5.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities4.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities5.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities4.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities4.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities5.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"title": "暑期名师领学,提高班级教学质量!高效冲分指南",
|
||||
"description": "本课程深度聚焦问题,让每一位教师了解并学习使用DeepSeek,结合办公自动化职业岗位标准,以实际工作任务为引导,强调课程内容的易用性和岗位要求的匹配性。",
|
||||
"thumbnail": "/images/courses/course-activities4.png",
|
||||
"price": 99,
|
||||
"originalPrice": 199,
|
||||
"currency": "CNY",
|
||||
"rating": 4.8,
|
||||
"ratingCount": 324,
|
||||
"studentsCount": 324,
|
||||
"duration": "12小时43分钟",
|
||||
"totalLessons": 54,
|
||||
"level": "beginner",
|
||||
"language": "zh-CN",
|
||||
"category": {
|
||||
"id": 1,
|
||||
"name": "教育培训",
|
||||
"slug": "education"
|
||||
},
|
||||
"tags": ["教学质量", "高效学习", "名师指导"],
|
||||
"skills": ["教学方法", "课堂管理", "学习指导"],
|
||||
"requirements": ["具备基本的计算机操作能力"],
|
||||
"objectives": ["掌握核心技能,能够在实际工作中熟练应用"],
|
||||
"instructor": {
|
||||
"id": 1,
|
||||
"name": "刘莹",
|
||||
"avatar": "https://via.placeholder.com/100",
|
||||
"bio": "资深教育专家",
|
||||
"title": "高级讲师",
|
||||
"rating": 4.8,
|
||||
"studentsCount": 1200,
|
||||
"coursesCount": 15,
|
||||
"experience": "10年教学经验",
|
||||
"education": ["教育学博士", "计算机科学硕士"],
|
||||
"certifications": ["高级讲师认证", "教学质量奖"]
|
||||
},
|
||||
"status": "published",
|
||||
"createdAt": "2025-01-26T10:13:17Z",
|
||||
"updatedAt": "2025-01-26T10:13:17Z"
|
||||
}
|
||||
]
|
||||
|
||||
// 根据ID获取课程
|
||||
export const getCourseById = (id: number): Course | undefined => {
|
||||
return mockCourses.find(course => course.id === id)
|
||||
}
|
||||
|
||||
// 获取热门课程(前5个)
|
||||
export const getPopularCourses = (): Course[] => {
|
||||
return mockCourses.slice(0, 4)
|
||||
}
|
||||
|
||||
// 根据分类筛选课程
|
||||
export const getCoursesByCategory = (categoryName: string): Course[] => {
|
||||
if (categoryName === '全部') return mockCourses
|
||||
return mockCourses.filter(course => course.category.name === categoryName)
|
||||
}
|
||||
|
||||
// 根据难度筛选课程
|
||||
export const getCoursesByLevel = (level: string): Course[] => {
|
||||
const levelMap: { [key: string]: string } = {
|
||||
'初级': 'beginner',
|
||||
'中级': 'intermediate',
|
||||
'高级': 'advanced'
|
||||
}
|
||||
const targetLevel = levelMap[level]
|
||||
if (!targetLevel) return mockCourses
|
||||
return mockCourses.filter(course => course.level === targetLevel)
|
||||
}
|
||||
|
||||
// 搜索课程
|
||||
export const searchCourses = (keyword: string): Course[] => {
|
||||
if (!keyword) return mockCourses
|
||||
const lowerKeyword = keyword.toLowerCase()
|
||||
return mockCourses.filter(course =>
|
||||
course.title.toLowerCase().includes(lowerKeyword) ||
|
||||
course.description.toLowerCase().includes(lowerKeyword) ||
|
||||
course.tags.some(tag => tag.toLowerCase().includes(lowerKeyword)) ||
|
||||
course.skills.some(skill => skill.toLowerCase().includes(lowerKeyword))
|
||||
)
|
||||
}
|
@ -7,7 +7,7 @@ export type Course = ApiCourse
|
||||
|
||||
export interface Lesson {
|
||||
id: number
|
||||
courseId: number
|
||||
courseId: string // 改为string类型,与Course.id保持一致
|
||||
title: string
|
||||
description: string
|
||||
videoUrl?: string
|
||||
@ -63,13 +63,13 @@ export const useCourseStore = defineStore('course', () => {
|
||||
console.log('尝试从API获取课程数据...')
|
||||
const response = await CourseApi.getCourses()
|
||||
console.log('API响应:', response)
|
||||
courses.value = response.data.list
|
||||
courses.value = response.data
|
||||
} catch (error) {
|
||||
console.error('API调用失败,使用模拟数据:', error)
|
||||
// 如果API调用失败,使用模拟数据作为后备
|
||||
const mockCourses: Course[] = [
|
||||
{
|
||||
id: 1,
|
||||
id: "1",
|
||||
title: 'Vue.js 3 完整教程',
|
||||
description: '从零开始学习Vue.js 3,包括Composition API、TypeScript集成等现代开发技术',
|
||||
content: '详细的Vue.js 3课程内容',
|
||||
@ -114,7 +114,7 @@ export const useCourseStore = defineStore('course', () => {
|
||||
publishedAt: '2024-01-01'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: "2",
|
||||
title: 'React 18 实战开发',
|
||||
description: '掌握React 18的新特性,包括并发渲染、Suspense等高级功能',
|
||||
content: '详细的React 18课程内容',
|
||||
@ -159,7 +159,7 @@ export const useCourseStore = defineStore('course', () => {
|
||||
publishedAt: '2024-01-05'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
id: "3",
|
||||
title: 'Node.js 后端开发',
|
||||
description: '学习Node.js后端开发,包括Express、数据库操作、API设计等',
|
||||
content: '详细的Node.js课程内容',
|
||||
@ -210,7 +210,7 @@ export const useCourseStore = defineStore('course', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCourseById = async (id: number) => {
|
||||
const fetchCourseById = async (id: string) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await CourseApi.getCourseById(id)
|
||||
@ -227,7 +227,7 @@ export const useCourseStore = defineStore('course', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchLessons = async (courseId: number) => {
|
||||
const fetchLessons = async (courseId: string) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
@ -271,7 +271,7 @@ export const useCourseStore = defineStore('course', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const enrollCourse = async (courseId: number) => {
|
||||
const enrollCourse = async (courseId: string) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
// 模拟API调用
|
||||
@ -292,7 +292,7 @@ export const useCourseStore = defineStore('course', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateProgress = async (courseId: number, progress: number) => {
|
||||
const updateProgress = async (courseId: string, progress: number) => {
|
||||
const course = enrolledCourses.value.find(c => c.id === courseId)
|
||||
if (course) {
|
||||
course.progress = progress
|
||||
|
@ -8,7 +8,7 @@ export interface User extends ApiUser {}
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// 状态
|
||||
const user = ref<User | null>(null)
|
||||
const token = ref<string | null>(localStorage.getItem('token'))
|
||||
const token = ref<string | null>(localStorage.getItem('X-Access-Token'))
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 计算属性
|
||||
@ -49,7 +49,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
// 无论API调用是否成功,都清除本地数据
|
||||
user.value = null
|
||||
token.value = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('X-Access-Token')
|
||||
localStorage.removeItem('refreshToken')
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('rememberMe')
|
||||
@ -136,7 +136,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
const initializeAuth = async () => {
|
||||
const savedUser = localStorage.getItem('user')
|
||||
const savedToken = localStorage.getItem('token')
|
||||
const savedToken = localStorage.getItem('X-Access-Token')
|
||||
|
||||
if (savedUser && savedToken) {
|
||||
try {
|
||||
|
@ -140,7 +140,15 @@
|
||||
<!-- 讲师信息 -->
|
||||
<div class="instructors-section">
|
||||
<h3 class="section-title">讲师</h3>
|
||||
<div class="instructors-list">
|
||||
|
||||
<div v-if="instructorsLoading" class="instructors-loading">
|
||||
<p>正在加载讲师信息...</p>
|
||||
</div>
|
||||
<div v-else-if="instructorsError" class="instructors-error">
|
||||
<p>{{ instructorsError }}</p>
|
||||
<button @click="loadCourseInstructors" class="retry-btn">重试</button>
|
||||
</div>
|
||||
<div v-else class="instructors-list">
|
||||
<div class="instructor-item" v-for="instructor in instructors" :key="instructor.id">
|
||||
<div class="instructor-avatar">
|
||||
<SafeAvatar :src="instructor.avatar" :name="instructor.name" :size="50" />
|
||||
@ -463,7 +471,7 @@ import RegisterModal from '@/components/auth/RegisterModal.vue'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const courseId = ref(Number(route.params.id))
|
||||
const courseId = ref(route.params.id as string)
|
||||
const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
|
||||
// enrollCourse 暂时未使用,后续需要时再启用
|
||||
|
||||
@ -480,6 +488,10 @@ const courseSections = ref<CourseSection[]>([])
|
||||
const sectionsLoading = ref(false)
|
||||
const sectionsError = ref('')
|
||||
|
||||
// 讲师数据
|
||||
const instructorsLoading = ref(false)
|
||||
const instructorsError = ref('')
|
||||
|
||||
// 报名状态管理
|
||||
const isEnrolled = ref(false) // 用户是否已报名该课程
|
||||
const enrollmentLoading = ref(false) // 报名加载状态
|
||||
@ -525,46 +537,12 @@ interface ChapterGroup {
|
||||
|
||||
const groupedSections = ref<ChapterGroup[]>([])
|
||||
|
||||
// 生成模拟章节数据(用于演示)
|
||||
const generateMockSections = (): CourseSection[] => {
|
||||
return [
|
||||
// 第一章 - 课前准备 (4个)
|
||||
{ id: 1, lessonId: courseId.value, name: '开课彩蛋:新开始新征程', outline: 'https://example.com/video1.m3u8', parentId: 0, sort: 1, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
|
||||
{ id: 2, lessonId: courseId.value, name: '课程定位与目标', outline: 'https://example.com/video2.m3u8', parentId: 0, sort: 2, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:44:05' },
|
||||
{ id: 3, lessonId: courseId.value, name: '教学安排及学习建议', outline: 'https://example.com/video3.m3u8', parentId: 0, sort: 3, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
|
||||
{ id: 4, lessonId: courseId.value, name: '课前准备PPT', outline: 'https://example.com/ppt1.ppt', parentId: 0, sort: 4, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
// 生成模拟章节数据(暂时禁用,等待API修复)
|
||||
// const generateMockSections = (): CourseSection[] => {
|
||||
// return []
|
||||
// }
|
||||
|
||||
// 第二章 - 程序设计基础知识 (5个)
|
||||
{ id: 5, lessonId: courseId.value, name: '第一课 程序设计入门', outline: 'https://example.com/video4.m3u8', parentId: 0, sort: 5, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
|
||||
{ id: 6, lessonId: courseId.value, name: '操作PPT', outline: 'https://example.com/ppt2.ppt', parentId: 0, sort: 6, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 7, lessonId: courseId.value, name: '第二课 循环结构', outline: 'https://example.com/video5.m3u8', parentId: 0, sort: 7, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
|
||||
{ id: 8, lessonId: courseId.value, name: '函数&循环', outline: 'https://example.com/video5.m3u8', parentId: 0, sort: 8, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 9, lessonId: courseId.value, name: '第三课 条件结构', outline: 'https://example.com/video6.m3u8', parentId: 0, sort: 9, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:45:30' },
|
||||
|
||||
// 第三章 - 实战项目 (6个)
|
||||
{ id: 10, lessonId: courseId.value, name: '项目一:计算器开发', outline: 'https://example.com/video7.m3u8', parentId: 0, sort: 10, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:20:15' },
|
||||
{ id: 11, lessonId: courseId.value, name: '项目源码下载', outline: 'https://example.com/source1.zip', parentId: 0, sort: 11, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 12, lessonId: courseId.value, name: '项目二:数据管理系统', outline: 'https://example.com/video8.m3u8', parentId: 0, sort: 12, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:45:20' },
|
||||
{ id: 13, lessonId: courseId.value, name: '作业:完成个人项目', outline: '', parentId: 0, sort: 13, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 14, lessonId: courseId.value, name: '项目三:Web应用开发', outline: 'https://example.com/video9.m3u8', parentId: 0, sort: 14, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '02:10:45' },
|
||||
{ id: 15, lessonId: courseId.value, name: '期末考试', outline: '', parentId: 0, sort: 15, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
|
||||
// 第四章 - 高级应用 (4个)
|
||||
{ id: 16, lessonId: courseId.value, name: '高级特性介绍', outline: 'https://example.com/video10.m3u8', parentId: 0, sort: 16, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:55:30' },
|
||||
{ id: 17, lessonId: courseId.value, name: '性能优化技巧', outline: 'https://example.com/video11.m3u8', parentId: 0, sort: 17, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:15:20' },
|
||||
{ id: 18, lessonId: courseId.value, name: '部署与发布', outline: 'https://example.com/video12.m3u8', parentId: 0, sort: 18, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:40:15' },
|
||||
{ id: 19, lessonId: courseId.value, name: '课程总结', outline: 'https://example.com/video13.m3u8', parentId: 0, sort: 19, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:30:10' },
|
||||
|
||||
// 第五章 - 拓展学习 (3个)
|
||||
{ id: 20, lessonId: courseId.value, name: '行业发展趋势', outline: 'https://example.com/video14.m3u8', parentId: 0, sort: 20, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:35:45' },
|
||||
{ id: 21, lessonId: courseId.value, name: '学习资源推荐', outline: 'https://example.com/resources.pdf', parentId: 0, sort: 21, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 22, lessonId: courseId.value, name: '结业证书申请', outline: '', parentId: 0, sort: 22, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
|
||||
// 第六章 - 答疑与交流 (2个)
|
||||
{ id: 23, lessonId: courseId.value, name: '常见问题解答', outline: 'https://example.com/video15.m3u8', parentId: 0, sort: 23, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:25:30' },
|
||||
{ id: 24, lessonId: courseId.value, name: '在线答疑直播', outline: 'https://example.com/live1.m3u8', parentId: 0, sort: 24, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:30:00' }
|
||||
]
|
||||
}
|
||||
|
||||
// 将章节按章分组
|
||||
const groupSectionsByChapter = (sections: CourseSection[]) => {
|
||||
@ -603,10 +581,10 @@ const groupSectionsByChapter = (sections: CourseSection[]) => {
|
||||
// console.log('没有章节数据,生成模拟数据')
|
||||
// courseSections.value = generateMockSections()
|
||||
// }
|
||||
|
||||
//
|
||||
// console.log('开始生成章节分组,原始数据:', courseSections.value)
|
||||
// console.log('章节数据数量:', courseSections.value.length)
|
||||
|
||||
//
|
||||
// // 使用统一的分组函数
|
||||
// groupedSections.value = groupSectionsByChapter(courseSections.value)
|
||||
// console.log('生成的章节分组:', groupedSections.value)
|
||||
@ -715,7 +693,7 @@ const displayComments = ref([
|
||||
const loadCourseDetail = async () => {
|
||||
console.log('开始加载课程详情,课程ID:', courseId.value)
|
||||
|
||||
if (!courseId.value || isNaN(courseId.value)) {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
error.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
@ -768,7 +746,7 @@ const loadCourseDetail = async () => {
|
||||
|
||||
// 加载课程章节列表
|
||||
const loadCourseSections = async () => {
|
||||
if (!courseId.value || isNaN(courseId.value)) {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
sectionsError.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
@ -783,17 +761,17 @@ const loadCourseSections = async () => {
|
||||
console.log('章节API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
courseSections.value = response.data
|
||||
groupedSections.value = groupSectionsByChapter(response.data)
|
||||
console.log('章节数据设置成功:', courseSections.value)
|
||||
console.log('分组数据:', groupedSections.value)
|
||||
if (response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||
courseSections.value = response.data.list
|
||||
groupedSections.value = groupSectionsByChapter(response.data.list)
|
||||
console.log('✅ 章节数据设置成功:', courseSections.value)
|
||||
console.log('✅ 分组数据:', groupedSections.value)
|
||||
} else {
|
||||
console.log('API返回的章节数据为空,使用模拟数据')
|
||||
console.log('⚠️ API返回的章节数据为空,使用模拟数据')
|
||||
loadMockData()
|
||||
}
|
||||
} else {
|
||||
console.log('API返回错误,使用模拟数据')
|
||||
console.log('⚠️ API返回错误,使用模拟数据')
|
||||
loadMockData()
|
||||
}
|
||||
} catch (err) {
|
||||
@ -807,15 +785,47 @@ const loadCourseSections = async () => {
|
||||
|
||||
// 加载模拟数据
|
||||
const loadMockData = () => {
|
||||
console.log('加载模拟章节数据')
|
||||
const mockSections = generateMockSections()
|
||||
courseSections.value = mockSections
|
||||
groupedSections.value = groupSectionsByChapter(mockSections)
|
||||
console.log('⚠️ API调用失败,暂不使用模拟数据')
|
||||
// 暂时不加载模拟数据,等待API修复
|
||||
courseSections.value = []
|
||||
groupedSections.value = []
|
||||
}
|
||||
|
||||
// 计算学习进度
|
||||
// const completed = mockSections.filter(section => section.completed).length
|
||||
// completedLessons.value = completed
|
||||
// progress.value = Math.round((completed / mockSections.length) * 100)
|
||||
// 加载课程讲师列表
|
||||
const loadCourseInstructors = async () => {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
instructorsError.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
instructorsLoading.value = true
|
||||
instructorsError.value = ''
|
||||
|
||||
console.log('调用API获取课程讲师...')
|
||||
const response = await CourseApi.getCourseInstructors(courseId.value)
|
||||
console.log('讲师API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
instructors.value = response.data
|
||||
console.log('✅ 讲师数据设置成功:', instructors.value)
|
||||
} else {
|
||||
console.log('⚠️ API返回的讲师数据为空,使用默认数据')
|
||||
// 保持默认的mock数据
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ API返回错误,使用默认数据')
|
||||
instructorsError.value = response.message || '获取讲师信息失败'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载课程讲师失败:', err)
|
||||
instructorsError.value = '获取讲师信息失败'
|
||||
// 保持默认的mock数据
|
||||
} finally {
|
||||
instructorsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换章节展开/折叠
|
||||
@ -859,7 +869,7 @@ const formatLessonDuration = (section: CourseSection): string => {
|
||||
]
|
||||
|
||||
// 根据section.id获取对应时长
|
||||
const durationIndex = section.id - 1
|
||||
const durationIndex = parseInt(section.id) - 1
|
||||
if (durationIndex >= 0 && durationIndex < durations.length) {
|
||||
return durations[durationIndex] || ''
|
||||
}
|
||||
@ -931,19 +941,8 @@ const handleDownload = (section: CourseSection) => {
|
||||
// 处理作业操作
|
||||
const handleHomework = (section: CourseSection) => {
|
||||
console.log('打开作业:', section)
|
||||
|
||||
// 跳转到练习页面
|
||||
router.push({
|
||||
name: 'Practice',
|
||||
params: {
|
||||
courseId: courseId.value,
|
||||
sectionId: section.id
|
||||
},
|
||||
query: {
|
||||
courseName: course.value?.title || '课程名称',
|
||||
practiceName: section.name
|
||||
}
|
||||
})
|
||||
// 这里可以跳转到作业页面
|
||||
alert(`打开作业: ${section.name}`)
|
||||
}
|
||||
|
||||
// 处理考试操作
|
||||
@ -1142,7 +1141,8 @@ onMounted(() => {
|
||||
console.log('课程详情页加载完成,课程ID:', courseId.value)
|
||||
initializeMockState() // 初始化模拟状态
|
||||
loadCourseDetail()
|
||||
loadCourseSections()
|
||||
loadCourseSections() // 启用章节接口调用
|
||||
loadCourseInstructors() // 启用讲师接口调用
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -1832,6 +1832,34 @@ onMounted(() => {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 讲师加载状态 */
|
||||
.instructors-loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.instructors-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.instructors-error .retry-btn {
|
||||
margin-top: 10px;
|
||||
padding: 6px 12px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.instructors-error .retry-btn:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
|
||||
/* 分隔线样式 */
|
||||
.course-info-divider {
|
||||
height: 1px;
|
||||
|
@ -49,6 +49,29 @@
|
||||
<p>请选择要播放的视频课程</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 清晰度选择器 -->
|
||||
<div v-if="videoQualities.length > 1" class="video-quality-selector">
|
||||
<div class="quality-dropdown">
|
||||
<button class="quality-btn" @click="showQualityMenu = !showQualityMenu">
|
||||
{{ currentQuality }}p
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" class="dropdown-icon">
|
||||
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showQualityMenu" class="quality-menu">
|
||||
<div
|
||||
v-for="quality in videoQualities"
|
||||
:key="quality.value"
|
||||
class="quality-option"
|
||||
:class="{ active: quality.value === currentQuality }"
|
||||
@click="changeVideoQuality(quality.value); showQualityMenu = false"
|
||||
>
|
||||
{{ quality.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部交互区域 -->
|
||||
@ -377,7 +400,7 @@ import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { CourseApi } from '@/api/modules/course'
|
||||
import type { Course, CourseSection } from '@/api/types'
|
||||
import type { Course, CourseSection, SectionVideo, VideoQuality } from '@/api/types'
|
||||
import SafeAvatar from '@/components/common/SafeAvatar.vue'
|
||||
import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
|
||||
import NotesModal from '@/components/common/NotesModal.vue'
|
||||
@ -395,7 +418,7 @@ declare global {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const courseId = ref(Number(route.params.id))
|
||||
const courseId = ref(route.params.id as string)
|
||||
|
||||
// 强制仅播放本地视频(如需关闭,置为 false)
|
||||
const FORCE_LOCAL_VIDEO = true
|
||||
@ -405,6 +428,13 @@ const currentSection = ref<CourseSection | null>(null)
|
||||
const currentVideoUrl = ref<string>('')
|
||||
const ckplayer = ref<any>(null)
|
||||
|
||||
// 视频相关状态
|
||||
const currentVideo = ref<SectionVideo | null>(null)
|
||||
const videoQualities = ref<VideoQuality[]>([])
|
||||
const currentQuality = ref<string>('360') // 默认360p
|
||||
const videoLoading = ref(false)
|
||||
const showQualityMenu = ref(false)
|
||||
|
||||
// 视频源配置
|
||||
const VIDEO_CONFIG = {
|
||||
// 本地视频(当前使用)
|
||||
@ -534,74 +564,50 @@ const displayComments = ref([
|
||||
}
|
||||
])
|
||||
|
||||
// 生成模拟章节数据(用于演示)
|
||||
// 生成模拟章节数据(暂时禁用)
|
||||
const generateMockSections = (): CourseSection[] => {
|
||||
return [
|
||||
// 第一章 - 课前准备 (4个)
|
||||
{ id: 1, lessonId: courseId.value, name: '开课彩蛋:新开始新征程', outline: 'https://example.com/video1.m3u8', parentId: 0, sort: 1, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
|
||||
{ id: 2, lessonId: courseId.value, name: '课程定位与目标', outline: 'https://example.com/video2.m3u8', parentId: 0, sort: 2, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:44:05' },
|
||||
{ id: 3, lessonId: courseId.value, name: '教学安排及学习建议', outline: 'https://example.com/video3.m3u8', parentId: 0, sort: 3, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
|
||||
{ id: 4, lessonId: courseId.value, name: '课前准备PPT', outline: 'https://example.com/ppt1.ppt', parentId: 0, sort: 4, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
|
||||
// 第二章 - 程序设计基础知识 (5个)
|
||||
{ id: 5, lessonId: courseId.value, name: '第一课 程序设计入门', outline: 'https://example.com/video4.m3u8', parentId: 0, sort: 5, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
|
||||
{ id: 6, lessonId: courseId.value, name: '操作PPT', outline: 'https://example.com/ppt2.ppt', parentId: 0, sort: 6, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 7, lessonId: courseId.value, name: '第二课 循环结构', outline: 'https://example.com/video5.m3u8', parentId: 0, sort: 7, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
|
||||
{ id: 8, lessonId: courseId.value, name: '函数&循环', outline: 'https://example.com/video5.m3u8', parentId: 0, sort: 8, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 9, lessonId: courseId.value, name: '第三课 条件结构', outline: 'https://example.com/video6.m3u8', parentId: 0, sort: 9, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:45:30' },
|
||||
|
||||
// 第三章 - 实战项目 (6个)
|
||||
{ id: 10, lessonId: courseId.value, name: '项目一:计算器开发', outline: 'https://example.com/video7.m3u8', parentId: 0, sort: 10, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:20:15' },
|
||||
{ id: 11, lessonId: courseId.value, name: '项目源码下载', outline: 'https://example.com/source1.zip', parentId: 0, sort: 11, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 12, lessonId: courseId.value, name: '项目二:数据管理系统', outline: 'https://example.com/video8.m3u8', parentId: 0, sort: 12, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:45:20' },
|
||||
{ id: 13, lessonId: courseId.value, name: '作业:完成个人项目', outline: '', parentId: 0, sort: 13, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 14, lessonId: courseId.value, name: '项目三:Web应用开发', outline: 'https://example.com/video9.m3u8', parentId: 0, sort: 14, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '02:10:45' },
|
||||
{ id: 15, lessonId: courseId.value, name: '期末考试', outline: '', parentId: 0, sort: 15, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
|
||||
// 第四章 - 高级应用 (4个)
|
||||
{ id: 16, lessonId: courseId.value, name: '高级特性介绍', outline: 'https://example.com/video10.m3u8', parentId: 0, sort: 16, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:55:30' },
|
||||
{ id: 17, lessonId: courseId.value, name: '性能优化技巧', outline: 'https://example.com/video11.m3u8', parentId: 0, sort: 17, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:15:20' },
|
||||
{ id: 18, lessonId: courseId.value, name: '部署与发布', outline: 'https://example.com/video12.m3u8', parentId: 0, sort: 18, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:40:15' },
|
||||
{ id: 19, lessonId: courseId.value, name: '课程总结', outline: 'https://example.com/video13.m3u8', parentId: 0, sort: 19, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:30:10' },
|
||||
|
||||
// 第五章 - 拓展学习 (3个)
|
||||
{ id: 20, lessonId: courseId.value, name: '行业发展趋势', outline: 'https://example.com/video14.m3u8', parentId: 0, sort: 20, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:35:45' },
|
||||
{ id: 21, lessonId: courseId.value, name: '学习资源推荐', outline: 'https://example.com/resources.pdf', parentId: 0, sort: 21, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
{ id: 22, lessonId: courseId.value, name: '结业证书申请', outline: '', parentId: 0, sort: 22, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
||||
|
||||
// 第六章 - 答疑与交流 (2个)
|
||||
{ id: 23, lessonId: courseId.value, name: '常见问题解答', outline: 'https://example.com/video15.m3u8', parentId: 0, sort: 23, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:25:30' },
|
||||
{ id: 24, lessonId: courseId.value, name: '在线答疑直播', outline: 'https://example.com/live1.m3u8', parentId: 0, sort: 24, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:30:00' }
|
||||
]
|
||||
// 暂时返回空数组,等待API修复
|
||||
return []
|
||||
}
|
||||
|
||||
// 将章节按章分组
|
||||
// 将章节按章分组 - 根据后端数据结构重新实现
|
||||
const groupSectionsByChapter = (sections: CourseSection[]) => {
|
||||
const chapterTitles = [
|
||||
'课前准备',
|
||||
'程序设计基础知识',
|
||||
'实战项目',
|
||||
'高级应用',
|
||||
'拓展学习',
|
||||
'答疑与交流'
|
||||
]
|
||||
console.log('🔍 开始分组章节数据:', sections)
|
||||
|
||||
const groups: ChapterGroup[] = []
|
||||
let sectionsPerChapter = [4, 5, 6, 4, 3, 2] // 每章的课程数量
|
||||
let sectionIndex = 0
|
||||
|
||||
for (let i = 0; i < chapterTitles.length; i++) {
|
||||
const chapterSections = sections.slice(sectionIndex, sectionIndex + sectionsPerChapter[i])
|
||||
if (chapterSections.length > 0) {
|
||||
groups.push({
|
||||
title: chapterTitles[i],
|
||||
sections: chapterSections,
|
||||
expanded: i === 0 // 默认展开第一章
|
||||
})
|
||||
}
|
||||
sectionIndex += sectionsPerChapter[i]
|
||||
// 找出所有一级章节(level=1,这些是父章节)
|
||||
const parentChapters = sections.filter(section => section.level === 1)
|
||||
console.log('🔍 找到一级章节:', parentChapters)
|
||||
|
||||
// 为每个一级章节创建分组
|
||||
parentChapters.forEach((parentChapter, index) => {
|
||||
// 找出该章节下的所有子章节(level=2,parentId匹配)
|
||||
const childSections = sections.filter(section =>
|
||||
section.level === 2 && section.parentId === parentChapter.id
|
||||
)
|
||||
|
||||
console.log(`🔍 章节"${parentChapter.name}"的子章节:`, childSections)
|
||||
|
||||
// 创建章节分组
|
||||
groups.push({
|
||||
title: parentChapter.name, // 使用后端返回的章节名称
|
||||
sections: childSections.length > 0 ? childSections : [parentChapter], // 如果有子章节就用子章节,否则用父章节本身
|
||||
expanded: index === 0 // 默认展开第一章
|
||||
})
|
||||
})
|
||||
|
||||
// 如果没有找到一级章节,可能所有章节都是同级的,直接作为一个组
|
||||
if (groups.length === 0 && sections.length > 0) {
|
||||
console.log('🔍 没有找到层级结构,将所有章节作为一组')
|
||||
groups.push({
|
||||
title: '课程章节',
|
||||
sections: sections,
|
||||
expanded: true
|
||||
})
|
||||
}
|
||||
|
||||
console.log('✅ 章节分组完成:', groups)
|
||||
return groups
|
||||
}
|
||||
|
||||
@ -609,7 +615,7 @@ const groupSectionsByChapter = (sections: CourseSection[]) => {
|
||||
const loadCourseDetail = async () => {
|
||||
console.log('开始加载课程详情,课程ID:', courseId.value)
|
||||
|
||||
if (!courseId.value || isNaN(courseId.value)) {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
error.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
@ -640,7 +646,7 @@ const loadCourseDetail = async () => {
|
||||
|
||||
// 加载课程章节列表
|
||||
const loadCourseSections = async () => {
|
||||
if (!courseId.value || isNaN(courseId.value)) {
|
||||
if (!courseId.value || courseId.value.trim() === '') {
|
||||
sectionsError.value = '课程ID无效'
|
||||
console.error('课程ID无效:', courseId.value)
|
||||
return
|
||||
@ -655,11 +661,11 @@ const loadCourseSections = async () => {
|
||||
console.log('章节API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
courseSections.value = response.data
|
||||
groupedSections.value = groupSectionsByChapter(response.data)
|
||||
console.log('章节数据设置成功:', courseSections.value)
|
||||
console.log('分组数据:', groupedSections.value)
|
||||
if (response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||
courseSections.value = response.data.list
|
||||
groupedSections.value = groupSectionsByChapter(response.data.list)
|
||||
console.log('✅ 章节数据设置成功:', courseSections.value)
|
||||
console.log('✅ 分组数据:', groupedSections.value)
|
||||
// 默认播放右侧第一个视频章节(当未强制使用本地视频时)
|
||||
if (!FORCE_LOCAL_VIDEO) {
|
||||
const firstVideo = courseSections.value.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4')))
|
||||
@ -721,6 +727,103 @@ const toggleChapter = (chapterIndex: number) => {
|
||||
groupedSections.value[chapterIndex].expanded = !groupedSections.value[chapterIndex].expanded
|
||||
}
|
||||
|
||||
// 加载章节视频
|
||||
const loadSectionVideo = async (section: CourseSection) => {
|
||||
try {
|
||||
videoLoading.value = true
|
||||
console.log('🔍 加载章节视频,章节ID:', section.id)
|
||||
|
||||
const response = await CourseApi.getSectionVideos(courseId.value, section.id)
|
||||
console.log('🔍 视频API响应:', response)
|
||||
|
||||
if (response.code === 0 || response.code === 200) {
|
||||
if (response.data && response.data.length > 0) {
|
||||
const video = response.data[0] // 取第一个视频
|
||||
currentVideo.value = video
|
||||
videoQualities.value = video.qualities
|
||||
currentQuality.value = video.defaultQuality
|
||||
|
||||
// 获取默认清晰度的URL
|
||||
const defaultQualityVideo = video.qualities.find(q => q.value === video.defaultQuality)
|
||||
if (defaultQualityVideo) {
|
||||
currentVideoUrl.value = defaultQualityVideo.url
|
||||
console.log('✅ 设置视频URL:', currentVideoUrl.value)
|
||||
|
||||
// 更新播放器
|
||||
await updateVideoPlayer()
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 没有找到视频数据')
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 获取视频失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载章节视频失败:', error)
|
||||
} finally {
|
||||
videoLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换视频清晰度
|
||||
const changeVideoQuality = async (quality: string) => {
|
||||
if (!currentVideo.value) return
|
||||
|
||||
const qualityVideo = currentVideo.value.qualities.find(q => q.value === quality)
|
||||
if (qualityVideo) {
|
||||
currentQuality.value = quality
|
||||
currentVideoUrl.value = qualityVideo.url
|
||||
console.log('🔍 切换清晰度到:', quality, 'URL:', qualityVideo.url)
|
||||
|
||||
// 更新播放器
|
||||
await updateVideoPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新视频播放器
|
||||
const updateVideoPlayer = async () => {
|
||||
if (!currentVideoUrl.value) {
|
||||
console.warn('⚠️ 视频URL为空,无法更新播放器')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔍 更新播放器视频源:', currentVideoUrl.value)
|
||||
|
||||
if (ckplayer.value) {
|
||||
// 尝试不同的CKPlayer API方法
|
||||
if (typeof ckplayer.value.newVideo === 'function') {
|
||||
console.log('✅ 使用newVideo方法更新视频源')
|
||||
ckplayer.value.newVideo(currentVideoUrl.value)
|
||||
} else if (typeof ckplayer.value.changeVideo === 'function') {
|
||||
console.log('✅ 使用changeVideo方法更新视频源')
|
||||
ckplayer.value.changeVideo(currentVideoUrl.value)
|
||||
} else if (typeof ckplayer.value.videoSrc === 'function') {
|
||||
console.log('✅ 使用videoSrc方法更新视频源')
|
||||
ckplayer.value.videoSrc(currentVideoUrl.value)
|
||||
} else {
|
||||
console.log('⚠️ 未找到合适的更新方法,重新初始化播放器')
|
||||
// 如果没有找到合适的方法,重新初始化播放器
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
}
|
||||
} else {
|
||||
console.log('🔍 播放器未初始化,开始初始化')
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 更新播放器失败:', error)
|
||||
// 如果更新失败,尝试重新初始化
|
||||
try {
|
||||
await nextTick()
|
||||
initCKPlayer(currentVideoUrl.value)
|
||||
} catch (initError) {
|
||||
console.error('❌ 重新初始化播放器也失败:', initError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取章节编号
|
||||
const getChapterNumber = (num: number) => {
|
||||
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
||||
@ -729,19 +832,39 @@ const getChapterNumber = (num: number) => {
|
||||
|
||||
// 课程类型判断函数
|
||||
const isVideoLesson = (section: CourseSection) => {
|
||||
console.log(section.outline)
|
||||
console.log('检查章节类型:', section.name, 'type:', section.type, 'outline:', section.outline)
|
||||
// 优先根据type字段判断:0=视频
|
||||
if (section.type === 0) {
|
||||
return true
|
||||
}
|
||||
// 如果type为null,则根据outline判断
|
||||
return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4'))
|
||||
}
|
||||
|
||||
const isResourceLesson = (section: CourseSection) => {
|
||||
// 优先根据type字段判断:1=资料
|
||||
if (section.type === 1) {
|
||||
return true
|
||||
}
|
||||
// 如果type为null,则根据outline或名称判断
|
||||
return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip'))
|
||||
}
|
||||
|
||||
const isHomeworkLesson = (section: CourseSection) => {
|
||||
// 优先根据type字段判断:3=作业
|
||||
if (section.type === 3) {
|
||||
return true
|
||||
}
|
||||
// 如果type为null,则根据名称判断
|
||||
return section.name.includes('作业') || section.name.includes('练习')
|
||||
}
|
||||
|
||||
const isExamLesson = (section: CourseSection) => {
|
||||
// 优先根据type字段判断:2=考试
|
||||
if (section.type === 2) {
|
||||
return true
|
||||
}
|
||||
// 如果type为null,则根据名称判断
|
||||
return section.name.includes('考试') || section.name.includes('测试')
|
||||
}
|
||||
|
||||
@ -771,18 +894,39 @@ const formatLessonDuration = (section: CourseSection) => {
|
||||
|
||||
// 处理章节点击 - 已报名状态,可以正常点击
|
||||
const handleSectionClick = (section: CourseSection) => {
|
||||
console.log('点击课程章节:', section.name)
|
||||
console.log('🔍 点击课程章节:', section.name, section)
|
||||
currentSection.value = section
|
||||
|
||||
// 如果是视频课程,直接播放
|
||||
if (isVideoLesson(section)) {
|
||||
handleVideoPlay(section)
|
||||
} else if (isResourceLesson(section)) {
|
||||
// 检查章节类型
|
||||
const isVideo = isVideoLesson(section)
|
||||
const isResource = isResourceLesson(section)
|
||||
const isHomework = isHomeworkLesson(section)
|
||||
const isExam = isExamLesson(section)
|
||||
|
||||
console.log('🔍 章节类型判断结果:', {
|
||||
isVideo,
|
||||
isResource,
|
||||
isHomework,
|
||||
isExam,
|
||||
type: section.type
|
||||
})
|
||||
|
||||
// 如果是视频课程,加载视频数据
|
||||
if (isVideo) {
|
||||
console.log('✅ 识别为视频课程,开始加载视频数据')
|
||||
loadSectionVideo(section)
|
||||
} else if (isResource) {
|
||||
console.log('✅ 识别为资料课程')
|
||||
handleDownload(section)
|
||||
} else if (isHomeworkLesson(section)) {
|
||||
} else if (isHomework) {
|
||||
console.log('✅ 识别为作业课程')
|
||||
handleHomework(section)
|
||||
} else if (isExamLesson(section)) {
|
||||
} else if (isExam) {
|
||||
console.log('✅ 识别为考试课程')
|
||||
handleExam(section)
|
||||
} else {
|
||||
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
|
||||
loadSectionVideo(section)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1158,6 +1302,73 @@ onUnmounted(() => {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 清晰度选择器 */
|
||||
.video-quality-selector {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.quality-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quality-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.quality-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.quality-btn:hover .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.quality-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
min-width: 80px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.quality-option {
|
||||
padding: 8px 12px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.quality-option:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.quality-option.active {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 课程信息区域 */
|
||||
.course-info-section {
|
||||
/* padding: 24px 0; */
|
||||
|
@ -368,7 +368,7 @@ import type { Course, CourseSection } from '@/api/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const courseId = ref(Number(route.params.id))
|
||||
const courseId = ref(route.params.id as string)
|
||||
|
||||
// 从URL查询参数获取视频信息
|
||||
const currentVideoUrl = ref(route.query.videoUrl ? decodeURIComponent(route.query.videoUrl as string) : '')
|
||||
@ -385,7 +385,7 @@ console.log('- 路由查询参数:', route.query)
|
||||
// 课程数据
|
||||
const course = ref<Course | null>(null)
|
||||
const courseSections = ref<CourseSection[]>([])
|
||||
const currentLessonId = ref(currentSectionId.value || 1)
|
||||
const currentLessonId = ref(currentSectionId.value ? currentSectionId.value.toString() : "1")
|
||||
|
||||
// 视频播放状态
|
||||
const isPlaying = ref(false)
|
||||
@ -476,12 +476,12 @@ const sortedChapters = computed(() => {
|
||||
|
||||
const hasPrevious = computed(() => {
|
||||
// 检查是否有上一节课
|
||||
return currentLessonId.value > 1
|
||||
return parseInt(currentLessonId.value) > 1
|
||||
})
|
||||
|
||||
const hasNext = computed(() => {
|
||||
// 检查是否有下一节课
|
||||
return currentLessonId.value < totalLessons.value
|
||||
return parseInt(currentLessonId.value) < totalLessons.value
|
||||
})
|
||||
|
||||
const isCurrentLessonCompleted = computed(() => {
|
||||
@ -498,7 +498,7 @@ watch(() => route.query, (newQuery) => {
|
||||
currentVideoTitle.value = decodeURIComponent(newQuery.sectionName as string)
|
||||
}
|
||||
if (newQuery.sectionId) {
|
||||
currentLessonId.value = Number(newQuery.sectionId)
|
||||
currentLessonId.value = newQuery.sectionId.toString()
|
||||
}
|
||||
})
|
||||
|
||||
@ -537,18 +537,19 @@ const generateChapterGroups = () => {
|
||||
title: '课前准备',
|
||||
lessons: [
|
||||
{
|
||||
id: 1,
|
||||
id: "1",
|
||||
lessonId: courseId.value,
|
||||
outline: currentVideoUrl.value,
|
||||
name: currentVideoTitle.value || '开课彩蛋:新开始新征程',
|
||||
parentId: 1,
|
||||
parentId: "1",
|
||||
sort: 0,
|
||||
level: 1,
|
||||
revision: 0,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: null,
|
||||
deletedAt: null,
|
||||
completed: false
|
||||
completed: false,
|
||||
type: 0
|
||||
}
|
||||
],
|
||||
expanded: true
|
||||
|
@ -17,70 +17,38 @@
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">类型:</span>
|
||||
<div class="filter-tags">
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '全部' }"
|
||||
@click="selectSubject('全部')">全部</span>
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '必修课' }"
|
||||
@click="selectSubject('必修课')">必修课</span>
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '高分课' }"
|
||||
@click="selectSubject('高分课')">高分课</span>
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '名师课堂' }"
|
||||
@click="selectSubject('名师课堂')">名师课堂</span>
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '训练营' }"
|
||||
@click="selectSubject('训练营')">训练营</span>
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '无考试' }"
|
||||
@click="selectSubject('无考试')">无考试</span>
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '专题讲座' }"
|
||||
@click="selectSubject('专题讲座')">专题讲座</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '全部' }"
|
||||
@click="selectMajor('全部')">全部</span>
|
||||
<span
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
class="filter-tag"
|
||||
:class="{ active: selectedMajor === category.name }"
|
||||
@click="selectMajor(category.name)"
|
||||
>
|
||||
{{ category.name }}
|
||||
</span>
|
||||
<!-- 加载状态 -->
|
||||
<span v-if="categoriesLoading" class="filter-tag loading">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 专题分类第一行 -->
|
||||
<!-- 专题分类 -->
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">专题:</span>
|
||||
<div class="filter-tags">
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '全部' }" @click="selectMajor('全部')">全部</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '学科教研' }"
|
||||
@click="selectMajor('学科教研')">学科教研</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '班级管理' }"
|
||||
@click="selectMajor('班级管理')">班级管理</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '通识技能' }"
|
||||
@click="selectMajor('通识技能')">通识技能</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '信息素养' }"
|
||||
@click="selectMajor('信息素养')">信息素养</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '师风师德' }"
|
||||
@click="selectMajor('师风师德')">师风师德</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '专题教育' }"
|
||||
@click="selectMajor('专题教育')">专题教育</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '综合实践' }"
|
||||
@click="selectMajor('综合实践')">综合实践</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '个人成长' }"
|
||||
@click="selectMajor('个人成长')">个人成长</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '教学案例' }"
|
||||
@click="selectMajor('教学案例')">教学案例</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '教育技术' }"
|
||||
@click="selectMajor('教育技术')">教育技术</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '心理健康' }"
|
||||
@click="selectMajor('心理健康')">心理健康</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '家校沟通' }"
|
||||
@click="selectMajor('家校沟通')">家校沟通</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '课程设计' }"
|
||||
@click="selectMajor('课程设计')">课程设计</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '教育政策' }"
|
||||
@click="selectMajor('教育政策')">教育政策</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '教学评估' }"
|
||||
@click="selectMajor('教学评估')">教学评估</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '创新教育' }"
|
||||
@click="selectMajor('创新教育')">创新教育</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === 'STEAM教育' }"
|
||||
@click="selectMajor('STEAM教育')">STEAM教育</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '教育心理学' }"
|
||||
@click="selectMajor('教育心理学')">教育心理学</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '差异化教学' }"
|
||||
@click="selectMajor('差异化教学')">差异化教学</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '教育领导力' }"
|
||||
@click="selectMajor('教育领导力')">教育领导力</span>
|
||||
<span class="filter-tag" :class="{ active: selectedMajor === '在线教学' }"
|
||||
@click="selectMajor('在线教学')">在线教学</span>
|
||||
<span class="filter-tag" :class="{ active: selectedSubject === '全部' }" @click="selectSubject('全部')">全部</span>
|
||||
<span
|
||||
v-for="subject in subjects"
|
||||
:key="subject.id"
|
||||
class="filter-tag"
|
||||
:class="{ active: selectedSubject === subject.name }"
|
||||
@click="selectSubject(subject.name)"
|
||||
>
|
||||
{{ subject.name }}
|
||||
</span>
|
||||
<!-- 加载状态 -->
|
||||
<span v-if="subjectsLoading" class="filter-tag loading">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -91,14 +59,17 @@
|
||||
<div class="filter-tags">
|
||||
<span class="filter-tag" :class="{ active: selectedDifficulty === '全部' }"
|
||||
@click="selectDifficulty('全部')">全部</span>
|
||||
<span class="filter-tag" :class="{ active: selectedDifficulty === '零基础' }"
|
||||
@click="selectDifficulty('零基础')">零基础</span>
|
||||
<span class="filter-tag" :class="{ active: selectedDifficulty === '初级' }"
|
||||
@click="selectDifficulty('初级')">初级</span>
|
||||
<span class="filter-tag" :class="{ active: selectedDifficulty === '中级' }"
|
||||
@click="selectDifficulty('中级')">中级</span>
|
||||
<span class="filter-tag" :class="{ active: selectedDifficulty === '高级' }"
|
||||
@click="selectDifficulty('高级')">高级</span>
|
||||
<span
|
||||
v-for="difficulty in difficulties"
|
||||
:key="difficulty.id"
|
||||
class="filter-tag"
|
||||
:class="{ active: selectedDifficulty === difficulty.name }"
|
||||
@click="selectDifficulty(difficulty.name)"
|
||||
>
|
||||
{{ difficulty.name }}
|
||||
</span>
|
||||
<!-- 加载状态 -->
|
||||
<span v-if="difficultiesLoading" class="filter-tag loading">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -118,9 +89,9 @@
|
||||
|
||||
<!-- 排序标签 -->
|
||||
<div class="sort-tabs">
|
||||
<span class="sort-tab" :class="{ active: selectedSort === '最新' }" @click="selectSort('最新')">最新</span>
|
||||
<span class="sort-tab" :class="{ active: selectedSort === '最热' }" @click="selectSort('最热')">最热</span>
|
||||
<span class="sort-tab" :class="{ active: selectedSort === '推荐' }" @click="selectSort('推荐')">推荐</span>
|
||||
<span class="sort-tab">最新</span>
|
||||
<span class="sort-tab">最热</span>
|
||||
<span class="sort-tab active">推荐</span>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
@ -203,8 +174,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { Course } from '@/api/types'
|
||||
import { mockCourses } from '@/data/mockCourses'
|
||||
import type { Course, CourseCategory, CourseSubject, CourseDifficulty } from '@/api/types'
|
||||
import { CourseApi } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@ -213,6 +184,18 @@ const courses = ref<Course[]>([])
|
||||
const loading = ref(false)
|
||||
const total = ref(0)
|
||||
|
||||
// 分类数据和加载状态
|
||||
const categories = ref<CourseCategory[]>([])
|
||||
const categoriesLoading = ref(false)
|
||||
|
||||
// 专题数据和加载状态
|
||||
const subjects = ref<CourseSubject[]>([])
|
||||
const subjectsLoading = ref(false)
|
||||
|
||||
// 难度数据和加载状态
|
||||
const difficulties = ref<CourseDifficulty[]>([])
|
||||
const difficultiesLoading = ref(false)
|
||||
|
||||
// 筛选状态
|
||||
const selectedSubject = ref('全部')
|
||||
const selectedMajor = ref('全部')
|
||||
@ -224,9 +207,6 @@ const itemsPerPage = 20
|
||||
const totalItems = computed(() => total.value)
|
||||
const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage))
|
||||
|
||||
// 排序相关状态
|
||||
const selectedSort = ref('推荐')
|
||||
|
||||
// 控制广告显示状态
|
||||
const showAdvertisement = ref(true)
|
||||
|
||||
@ -276,84 +256,59 @@ const visiblePages = computed(() => {
|
||||
return pages
|
||||
})
|
||||
|
||||
// 排序切换函数
|
||||
const selectSort = (sort: string) => {
|
||||
selectedSort.value = sort
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
loadCourses()
|
||||
}
|
||||
|
||||
// 加载课程数据(使用模拟数据)
|
||||
// 加载课程数据(使用真实API)
|
||||
const loadCourses = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
console.log('🚀 加载课程数据...')
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
// 构建查询参数
|
||||
const queryParams: any = {}
|
||||
|
||||
// 筛选逻辑
|
||||
let filteredCourses = [...mockCourses]
|
||||
|
||||
// 按学科筛选
|
||||
if (selectedSubject.value !== '全部') {
|
||||
filteredCourses = filteredCourses.filter(course => {
|
||||
switch (selectedSubject.value) {
|
||||
case '计算机':
|
||||
return course.category.name === '编程开发' || course.category.name === '前端开发' || course.category.name === '后端开发'
|
||||
case '教育学':
|
||||
return course.category.name === '教育培训'
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 排序逻辑
|
||||
switch (selectedSort.value) {
|
||||
case '最新':
|
||||
filteredCourses.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
break
|
||||
case '最热':
|
||||
filteredCourses.sort((a, b) => b.studentsCount - a.studentsCount)
|
||||
break
|
||||
case '推荐':
|
||||
default:
|
||||
// 推荐排序可以根据评分和学生数综合排序
|
||||
filteredCourses.sort((a, b) => (b.rating * 0.7 + b.studentsCount * 0.3) - (a.rating * 0.7 + a.studentsCount * 0.3))
|
||||
break
|
||||
}
|
||||
|
||||
// 按专业筛选
|
||||
// 根据选择的分类添加categoryId参数(分类接口返回的是{id, name}格式,传递id字段)
|
||||
if (selectedMajor.value !== '全部') {
|
||||
filteredCourses = filteredCourses.filter(course =>
|
||||
course.title.includes(selectedMajor.value) ||
|
||||
course.description.includes(selectedMajor.value) ||
|
||||
course.tags.some(tag => tag.includes(selectedMajor.value))
|
||||
)
|
||||
const selectedCategory = categories.value.find(cat => cat.name === selectedMajor.value)
|
||||
if (selectedCategory) {
|
||||
queryParams.categoryId = selectedCategory.id.toString()
|
||||
console.log('🏷️ 选择的分类:', selectedCategory.name, 'ID:', selectedCategory.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 按难度筛选
|
||||
// 根据选择的难度添加difficulty参数(难度接口返回的是{value, label}格式,传递value字段)
|
||||
if (selectedDifficulty.value !== '全部') {
|
||||
const difficultyMap: { [key: string]: string } = {
|
||||
'初级': 'beginner',
|
||||
'中级': 'intermediate',
|
||||
'高级': 'advanced'
|
||||
}
|
||||
const targetLevel = difficultyMap[selectedDifficulty.value]
|
||||
if (targetLevel) {
|
||||
filteredCourses = filteredCourses.filter(course => course.level === targetLevel)
|
||||
const selectedDiff = difficulties.value.find(diff => diff.name === selectedDifficulty.value)
|
||||
if (selectedDiff) {
|
||||
queryParams.difficulty = selectedDiff.id // 直接使用字符串值,不需要toString()
|
||||
console.log('📊 选择的难度:', selectedDiff.name, 'Value:', selectedDiff.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
total.value = filteredCourses.length
|
||||
const startIndex = (currentPage.value - 1) * itemsPerPage
|
||||
const endIndex = startIndex + itemsPerPage
|
||||
courses.value = filteredCourses.slice(startIndex, endIndex)
|
||||
// 根据选择的专题添加subject参数(专题接口返回的是{value, label}格式,传递value字段)
|
||||
if (selectedSubject.value !== '全部') {
|
||||
const selectedSubj = subjects.value.find(subj => subj.name === selectedSubject.value)
|
||||
if (selectedSubj) {
|
||||
queryParams.subject = selectedSubj.id // 直接使用字符串值,不需要toString()
|
||||
console.log('🎯 选择的专题:', selectedSubj.name, 'Value:', selectedSubj.id)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('课程加载成功:', courses.value.length, '条课程,总计:', total.value)
|
||||
console.log('🔍 查询参数:', queryParams)
|
||||
|
||||
// 调用API
|
||||
const response = await CourseApi.getCourses(queryParams)
|
||||
console.log('✅ 课程API响应:', response)
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
courses.value = response.data
|
||||
total.value = response.data.length
|
||||
console.log('✅ 课程数据加载成功:', courses.value.length, '条课程')
|
||||
} else {
|
||||
console.warn('⚠️ 课程数据加载失败:', response.message)
|
||||
courses.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载课程失败:', error)
|
||||
console.error('❌ 加载课程失败:', error)
|
||||
courses.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
@ -442,9 +397,78 @@ const goToCourseDetail = (course: Course) => {
|
||||
})
|
||||
}
|
||||
|
||||
// 加载课程分类数据
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
categoriesLoading.value = true
|
||||
console.log('🚀 加载课程分类...')
|
||||
|
||||
const response = await CourseApi.getCategories()
|
||||
console.log('✅ 分类API响应:', response)
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
categories.value = response.data
|
||||
console.log('✅ 分类数据加载成功:', categories.value)
|
||||
} else {
|
||||
console.warn('⚠️ 分类数据加载失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载分类数据失败:', error)
|
||||
} finally {
|
||||
categoriesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程专题数据
|
||||
const loadSubjects = async () => {
|
||||
try {
|
||||
subjectsLoading.value = true
|
||||
console.log('🚀 加载课程专题...')
|
||||
|
||||
const response = await CourseApi.getSubjects()
|
||||
console.log('✅ 专题API响应:', response)
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
subjects.value = response.data
|
||||
console.log('✅ 专题数据加载成功:', subjects.value)
|
||||
} else {
|
||||
console.warn('⚠️ 专题数据加载失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载专题数据失败:', error)
|
||||
} finally {
|
||||
subjectsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载课程难度数据
|
||||
const loadDifficulties = async () => {
|
||||
try {
|
||||
difficultiesLoading.value = true
|
||||
console.log('🚀 加载课程难度...')
|
||||
|
||||
const response = await CourseApi.getDifficulties()
|
||||
console.log('✅ 难度API响应:', response)
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
difficulties.value = response.data
|
||||
console.log('✅ 难度数据加载成功:', difficulties.value)
|
||||
} else {
|
||||
console.warn('⚠️ 难度数据加载失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载难度数据失败:', error)
|
||||
} finally {
|
||||
difficultiesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadCourses()
|
||||
loadCategories()
|
||||
loadSubjects()
|
||||
loadDifficulties()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -617,6 +641,13 @@ onMounted(() => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-tag.loading {
|
||||
background: #f0f0f0;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sort-tabs {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
@ -528,7 +528,7 @@ import { useCourseStore } from '@/stores/course'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import LoginModal from '@/components/auth/LoginModal.vue'
|
||||
import RegisterModal from '@/components/auth/RegisterModal.vue'
|
||||
import { getPopularCourses } from '@/data/mockCourses'
|
||||
// import { getPopularCourses } from '@/data/mockCourses'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const router = useRouter()
|
||||
@ -554,8 +554,8 @@ const bannerAlt = computed(() => {
|
||||
|
||||
// 热门课程数据
|
||||
const popularCourses = computed(() => {
|
||||
const courses = getPopularCourses()
|
||||
return courses.map(course => ({
|
||||
const courses = courseStore.courses.slice(0, 4) // 取前4个课程作为热门课程
|
||||
return courses.map((course: any) => ({
|
||||
id: course.id,
|
||||
title: course.title,
|
||||
thumbnail: course.thumbnail,
|
||||
@ -719,12 +719,12 @@ const partners = computed(() => [
|
||||
// ])
|
||||
|
||||
// 跳转到课程详情页面
|
||||
const goToCourseDetail = (courseId: number) => {
|
||||
const goToCourseDetail = (courseId: string) => {
|
||||
router.push(`/course/${courseId}`)
|
||||
}
|
||||
|
||||
// 处理课程报名 - 跳转到课程详情页面
|
||||
const handleEnrollCourse = (courseId: number) => {
|
||||
const handleEnrollCourse = (courseId: string) => {
|
||||
// 跳转到课程详情页面,在那里进行登录状态判断和报名
|
||||
router.push(`/course/${courseId}`)
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ const isPlaying = ref(false)
|
||||
const noteContent = ref('')
|
||||
const progress = ref(0)
|
||||
|
||||
const courseId = computed(() => Number(route.params.id))
|
||||
const courseId = computed(() => route.params.id as string)
|
||||
const course = computed(() => courseStore.currentCourse)
|
||||
const lessons = computed(() => courseStore.lessons)
|
||||
|
||||
|
@ -128,7 +128,7 @@ const toggleFollow = () => {
|
||||
}
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { Course } from '@/api/types'
|
||||
import { mockCourses } from '@/data/mockCourses'
|
||||
// import { mockCourses } from '@/data/mockCourses'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@ -201,7 +201,7 @@ const loadCourses = async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 筛选逻辑
|
||||
let filteredCourses = [...mockCourses]
|
||||
let filteredCourses: Course[] = [] // 暂时使用空数组,后续可以从API获取
|
||||
|
||||
// 按学科筛选
|
||||
if (selectedSubject.value !== '全部') {
|
||||
@ -234,7 +234,7 @@ const loadCourses = async () => {
|
||||
});
|
||||
} else {
|
||||
// 默认排序(按课程ID)
|
||||
filteredCourses.sort((a, b) => a.id - b.id);
|
||||
filteredCourses.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
// 按专业筛选
|
||||
@ -242,7 +242,7 @@ const loadCourses = async () => {
|
||||
filteredCourses = filteredCourses.filter(course =>
|
||||
course.title.includes(selectedMajor.value) ||
|
||||
course.description.includes(selectedMajor.value) ||
|
||||
course.tags.some(tag => tag.includes(selectedMajor.value))
|
||||
course.tags?.some((tag: string) => tag.includes(selectedMajor.value))
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ import { ref } from 'vue'
|
||||
import { CourseApi } from '@/api/modules/course'
|
||||
import type { CourseSection } from '@/api/types'
|
||||
|
||||
const testLessonId = ref(1)
|
||||
const testLessonId = ref("1")
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const sections = ref<CourseSection[]>([])
|
||||
|
@ -1,35 +1,27 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const proxyTarget = env.VITE_PROXY_TARGET || 'http://110.42.96.65:55510'
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
// 将以 /api 开头的请求代理到后端,避免浏览器CORS限制
|
||||
'/api': {
|
||||
target: proxyTarget,
|
||||
changeOrigin: true,
|
||||
// 如果后端接口不是以 /api 开头,可在这里改写路径
|
||||
// rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/jeecgboot': {
|
||||
target: 'http://103.40.14.23:25526',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user