This commit is contained in:
Wxp 2025-08-16 20:05:35 +08:00
commit 94ee525ce6
22 changed files with 1502 additions and 1618 deletions

2
.env
View File

@ -1,5 +1,5 @@
# API配置 # 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 # Mock配置 - 禁用Mock使用真实API
VITE_ENABLE_MOCK=false VITE_ENABLE_MOCK=false

View File

@ -1,7 +1,7 @@
# 开发环境配置 # 开发环境配置
# API配置 # 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配置
# 设置为 true 使用Mock数据false 使用真实API # 设置为 true 使用Mock数据false 使用真实API

View File

@ -1,7 +1,7 @@
# 生产环境配置 # 生产环境配置
# API配置 # 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 # Mock配置 - 生产环境禁用Mock使用真实API
VITE_ENABLE_MOCK=false VITE_ENABLE_MOCK=false

View File

@ -73,15 +73,14 @@ export const getCoursesExample = async () => {
const response = await CourseApi.getCourses({ const response = await CourseApi.getCourses({
page: 1, page: 1,
pageSize: 20, pageSize: 20,
categoryId: 1, categoryId: '1',
difficulty: 1, difficulty: '1'
sortBy: 'createdAt'
}) })
if (response.code === 200) { if (response.code === 200) {
const { list, total } = response.data const courses = response.data
console.log('课程列表:', list) console.log('课程列表:', courses)
console.log('总数:', total) console.log('总数:', courses.length)
return response.data return response.data
} }
} catch (error) { } catch (error) {
@ -113,7 +112,7 @@ export const searchCoursesExample = async () => {
} }
// 获取课程详情示例 // 获取课程详情示例
export const getCourseDetailExample = async (courseId: number) => { export const getCourseDetailExample = async (courseId: string) => {
try { try {
const response = await CourseApi.getCourseById(courseId) const response = await CourseApi.getCourseById(courseId)
if (response.code === 200) { if (response.code === 200) {

View File

@ -12,13 +12,13 @@ export { default as UploadApi } from './modules/upload'
export { default as StatisticsApi } from './modules/statistics' export { default as StatisticsApi } from './modules/statistics'
// API 基础配置 // 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 端点配置 // API 端点配置
export const API_ENDPOINTS = { export const API_ENDPOINTS = {
// 认证相关 // 认证相关
AUTH: { AUTH: {
LOGIN: '/auth/login', LOGIN: '/biz/user/login',
REGISTER: '/auth/register', REGISTER: '/auth/register',
LOGOUT: '/auth/logout', LOGOUT: '/auth/logout',
REFRESH: '/auth/refresh', REFRESH: '/auth/refresh',
@ -34,12 +34,12 @@ export const API_ENDPOINTS = {
// 课程相关 // 课程相关
COURSES: { COURSES: {
LIST: '/courses', LIST: '/biz/course/list',
SEARCH: '/courses/search', SEARCH: '/courses/search',
POPULAR: '/courses/popular', POPULAR: '/courses/popular',
LATEST: '/courses/latest', LATEST: '/courses/latest',
RECOMMENDED: '/courses/recommended', RECOMMENDED: '/courses/recommended',
DETAIL: '/courses/:id', DETAIL: '/biz/course/detail',
CHAPTERS: '/courses/:id/chapters', CHAPTERS: '/courses/:id/chapters',
LESSONS: '/courses/:id/lessons', LESSONS: '/courses/:id/lessons',
ENROLL: '/courses/:id/enroll', ENROLL: '/courses/:id/enroll',
@ -52,10 +52,20 @@ export const API_ENDPOINTS = {
// 分类相关 // 分类相关
CATEGORIES: { CATEGORIES: {
LIST: '/categories', LIST: '/biz/course/category/list',
COURSES: '/categories/:id/courses', COURSES: '/categories/:id/courses',
}, },
// 专题相关
SUBJECTS: {
LIST: '/biz/course/subject/list',
},
// 难度相关
DIFFICULTIES: {
LIST: '/biz/course/difficulty/list',
},
// 章节课时相关 // 章节课时相关
CHAPTERS: { CHAPTERS: {
DETAIL: '/chapters/:id', DETAIL: '/chapters/:id',

View File

@ -17,10 +17,16 @@ export class AuthApi {
// 用户登录 // 用户登录
static async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> { static async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
try { 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 // 调用后端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('🔍 Login API Response:', response)
console.log('🔍 Response Code:', response.code) console.log('🔍 Response Code:', response.code)
@ -34,12 +40,19 @@ export class AuthApi {
// 如果response.code是undefined检查response.data是否包含完整的API响应 // 如果response.code是undefined检查response.data是否包含完整的API响应
if (actualCode === undefined && actualData && typeof actualData === 'object') { if (actualCode === undefined && actualData && typeof actualData === 'object') {
if ('code' in actualData && 'message' in actualData && 'data' in actualData) { // 检查是否是标准的jeecg-boot响应格式 (success, code, message, result)
// 这种情况下真正的API响应被包装在了response.data中 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 actualCode = actualData.code
actualMessage = actualData.message actualMessage = actualData.message
actualData = actualData.data 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> } as ApiResponse<LoginResponse>
} }
// 如果后端返回的是真实API格式包含token, timestamp, expires // 如果后端返回的是jeecg-boot格式只包含token
if (actualData && actualData.token && actualData.timestamp) { if (actualData && actualData.token) {
const adaptedResponse: ApiResponse<LoginResponse> = { const adaptedResponse: ApiResponse<LoginResponse> = {
code: actualCode, code: actualCode,
message: actualMessage || '登录成功', message: actualMessage || '登录成功',
@ -64,7 +77,7 @@ export class AuthApi {
id: 1, // 真实API没有返回用户ID使用默认值 id: 1, // 真实API没有返回用户ID使用默认值
email: data.email || '', email: data.email || '',
phone: data.phone || '', phone: data.phone || '',
username: data.phone || data.email?.split('@')[0] || 'user', username: data.email || data.phone || '',
nickname: '用户', nickname: '用户',
avatar: '', avatar: '',
role: 'student', role: 'student',
@ -216,7 +229,7 @@ export class AuthApi {
// 获取当前用户信息 // 获取当前用户信息
static getCurrentUser(): Promise<ApiResponse<User>> { static getCurrentUser(): Promise<ApiResponse<User>> {
return ApiRequest.get('/auth/me') return ApiRequest.get('/users/info')
} }
// 更新用户资料 // 更新用户资料

View File

@ -5,20 +5,24 @@ import type {
PaginationResponse, PaginationResponse,
Course, Course,
CourseCategory, CourseCategory,
CourseSubject,
CourseDifficulty,
CourseListQueryParams,
BackendCourseItem,
Chapter, Chapter,
Lesson, Lesson,
LessonResource, LessonResource,
CourseSection, CourseSection,
CourseSectionListResponse, CourseSectionListResponse,
BackendCourseSection, BackendCourseSection,
BackendCourseSectionListResponse, BackendInstructor,
BackendSectionVideo,
SectionVideo,
VideoQuality,
Quiz, Quiz,
LearningProgress, LearningProgress,
SearchRequest, SearchRequest,
Instructor, Instructor,
BackendCourse,
BackendCourseListResponse,
CourseListRequest,
} from '../types' } from '../types'
/** /**
@ -62,142 +66,97 @@ export class CourseApi {
} }
} }
/** /**
* *
*/ */
private static calculateDuration(startTime: string, endTime: string): string { private static mapDifficultyToLevel(difficulty: number): string {
try { switch (difficulty) {
const start = new Date(startTime) case 0: return '零基础'
const end = new Date(endTime) case 1: return '初级'
const diffMs = end.getTime() - start.getTime() case 2: return '进阶'
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24)) case 3: return '高阶'
return `${diffDays}` default: return '未知'
} catch (error) { }
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 { try {
console.log('调用课程列表API参数:', params) console.log('🚀 调用课程列表API参数:', params)
// 构建查询参数根据API文档的参数名称 // 构建查询参数根据API文档的参数名称
const queryParams: any = {} const queryParams: any = {}
if (params?.categoryId) queryParams.categoryId = params.categoryId 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?.subject) queryParams.subject = params.subject
if (params?.page) queryParams.page = params.page
if (params?.pageSize) queryParams.pageSize = params.pageSize console.log('🔍 查询参数:', queryParams)
if (params?.keyword) queryParams.keyword = params.keyword
// 调用后端API // 调用后端API
const response = await ApiRequest.get<BackendCourseListResponse>('/lesson/list', queryParams) const response = await ApiRequest.get<any>('/biz/course/list', queryParams)
console.log('课程列表API响应:', response) console.log('🔍 课程列表API响应:', response)
console.log('响应数据结构:', response.data)
console.log('响应数据类型:', typeof response.data)
// 检查是否是axios响应格式还是我们的ApiResponse格式 // 处理后端响应格式
let actualData: any if (response.data && response.data.success && response.data.result) {
let actualCode: number // 转换后端数据格式为前端格式
let actualMessage: string 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 { return {
code: actualCode || 0, code: 200,
message: actualMessage || '数据格式异常', message: '获取成功',
data: { data: courses
list: [], }
total: 0, } else {
page: params?.page || 1, console.warn('⚠️ 课程列表API返回格式异常:', response)
pageSize: params?.pageSize || 10, return {
totalPages: 0 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) { } catch (error: any) {
console.error('课程API调用失败:', error) console.error('课程API调用失败:', error)
@ -214,13 +173,96 @@ export class CourseApi {
return { return {
code: 500, code: 500,
message: errorMessage, message: errorMessage,
data: { data: []
list: [], }
total: 0, }
page: params?.page || 1, }
pageSize: params?.pageSize || 10,
totalPages: 0 // 获取课程详情 - 适配后端接口
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 { try {
// 调用后端课程详情接口 // 调用后端课程详情接口
const response = await ApiRequest.get<BackendCourse>('/lesson/detail', { id }) const response = await ApiRequest.get<any>('/biz/course/detail', { id })
// 检查是否是axios响应格式还是我们的ApiResponse格式 console.log('🔍 课程详情API响应:', response)
let actualData: any
let actualCode: number
let actualMessage: string
// 使用类型断言来处理不同的响应格式 // 处理后端响应格式
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) { return {
// 这是我们期望的ApiResponse格式: { code, message, data: BackendCourse } code: 200,
actualData = responseAny.data.data message: '获取成功',
actualCode = responseAny.data.code data: course
actualMessage = responseAny.data.message }
console.log('检测到ApiResponse格式')
} else { } else {
// 这可能是直接的axios响应格式: BackendCourse console.warn('⚠️ 课程详情API返回格式异常:', response)
actualData = responseAny.data return {
actualCode = responseAny.status || 200 code: 500,
actualMessage = responseAny.statusText || 'OK' message: response.data?.message || '获取课程详情失败',
console.log('检测到直接响应格式') data: {} as Course
}
} }
} catch (error: any) {
// 适配数据格式 console.error('❌ 获取课程详情失败:', error)
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
}
return { return {
code: actualCode, code: 500,
message: actualMessage, message: error.message || '获取课程详情失败',
data: adaptedCourse data: {} as Course
} }
} catch (error) {
throw error
} }
} }
@ -355,8 +400,135 @@ export class CourseApi {
} }
// 获取课程分类 // 获取课程分类
static getCategories(): Promise<ApiResponse<CourseCategory[]>> { static async getCategories(): Promise<ApiResponse<CourseCategory[]>> {
return ApiRequest.get('/categories') 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 { try {
console.log('尝试从API获取课程章节数据课程ID:', lessonId) console.log('🔍 获取课程章节数据课程ID:', courseId)
console.log('API请求URL: /lesson/section/list') console.log('🔍 API请求URL: /biz/course/' + courseId + '/section')
console.log('API请求参数:', { lesson_id: lessonId.toString() })
const backendResponse = await ApiRequest.get<BackendCourseSectionListResponse>('/lesson/section/list', { lesson_id: lessonId.toString() }) const response = await ApiRequest.get<any>(`/biz/course/${courseId}/section`)
console.log('章节API响应:', backendResponse) console.log('🔍 章节API响应:', response)
// 检查是否是axios响应格式还是我们的ApiResponse格式 // 处理后端响应格式
let actualData: any if (response.data && response.data.success && response.data.result) {
let actualCode: number console.log('✅ 响应状态码:', response.data.code)
let actualMessage: string console.log('✅ 响应消息:', response.data.message)
let actualTimestamp: string | undefined 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) { console.log('✅ 适配后的章节数据:', adaptedSections)
// 这是我们期望的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('响应状态码:', 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 { return {
code: actualCode, code: response.data.code,
message: actualMessage, 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: { data: {
list: [], list: [],
timestamp: Date.now(), timestamp: Date.now(),
traceId: actualTimestamp || '' traceId: ''
}, }
timestamp: actualTimestamp
} }
} }
// 适配数据格式
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) { } catch (error) {
console.error('章节API调用失败:', error) console.error('❌ 章节API调用失败:', error)
console.error('错误详情:', { console.error('❌ 错误详情:', {
message: (error as Error).message, message: (error as Error).message,
stack: (error as Error).stack, stack: (error as Error).stack,
response: (error as any).response?.data, response: (error as any).response?.data,
@ -622,33 +765,189 @@ export class CourseApi {
return ApiRequest.get(`/courses/${courseId}/access`) return ApiRequest.get(`/courses/${courseId}/access`)
} }
// 辅助方法:映射难度等级 // 获取课程讲师列表
private static mapDifficulty(difficulty: number): 'beginner' | 'intermediate' | 'advanced' { static async getCourseInstructors(courseId: string): Promise<ApiResponse<Instructor[]>> {
switch (difficulty) { try {
case 0: console.log('🔍 获取课程讲师数据课程ID:', courseId)
return 'beginner' console.log('🔍 API请求URL: /biz/course/' + courseId + '/teachers')
case 1:
return 'intermediate' const response = await ApiRequest.get<any>(`/biz/course/${courseId}/teachers`)
case 2: console.log('🔍 讲师API响应:', response)
return 'advanced'
default: // 处理后端响应格式
return 'beginner' 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 { static async getSectionVideos(courseId: string, sectionId: string): Promise<ApiResponse<SectionVideo[]>> {
// 这里可以根据categoryId返回对应的分类名称 try {
// 暂时返回默认值后续可以通过分类API获取 console.log('🔍 获取章节视频数据课程ID:', courseId, '章节ID:', sectionId)
const categoryMap: { [key: number]: string } = { console.log('🔍 API请求URL: /biz/course/' + courseId + '/section_video/' + sectionId)
1: '信息技术',
2: '数学', const response = await ApiRequest.get<any>(`/biz/course/${courseId}/section_video/${sectionId}`)
3: '物理', console.log('🔍 章节视频API响应:', response)
4: '化学',
5: '生物' // 处理后端响应格式
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 export default CourseApi

View File

@ -12,9 +12,6 @@ const checkNetworkStatus = (): boolean => {
return true // 默认认为网络可用 return true // 默认认为网络可用
} }
// 全局开关强制使用Mock数据当前需求全部使用Mock不访问后端
const FORCE_ENABLE_MOCK = true
// 消息提示函数 - 使用window.alert作为fallback实际项目中应该使用UI库的消息组件 // 消息提示函数 - 使用window.alert作为fallback实际项目中应该使用UI库的消息组件
const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => { const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
// 这里可以替换为你使用的UI库的消息组件 // 这里可以替换为你使用的UI库的消息组件
@ -30,21 +27,36 @@ const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'i
// 创建axios实例 // 创建axios实例
const request: AxiosInstance = axios.create({ const request: AxiosInstance = axios.create({
// 统一走 /api由 Vite 代理到后端避免CORS若启用Mock则只会走本地Mock baseURL: import.meta.env.VITE_API_BASE_URL || '/jeecgboot',
baseURL: '/api',
timeout: 30000, // 增加到30秒 timeout: 30000, // 增加到30秒
headers: { headers: {
'Content-Type': 'application/json', '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( request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
// 添加认证token // 检查是否需要添加token
const userStore = useUserStore() const needToken = !NO_TOKEN_URLS.some(url => config.url?.includes(url))
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}` 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>( const retryRequest = async <T = any>(
@ -459,19 +236,7 @@ export class ApiRequest {
params?: any, params?: any,
config?: AxiosRequestConfig config?: AxiosRequestConfig
): Promise<ApiResponse<T>> { ): Promise<ApiResponse<T>> {
const enableMock = FORCE_ENABLE_MOCK || ((import.meta as any).env?.VITE_ENABLE_MOCK === 'true') return await retryRequest(() => request.get(url, { params, ...config }))
// 优先若显式启用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)
}
} }
// POST 请求 // POST 请求
@ -480,17 +245,7 @@ export class ApiRequest {
data?: any, data?: any,
config?: AxiosRequestConfig config?: AxiosRequestConfig
): Promise<ApiResponse<T>> { ): Promise<ApiResponse<T>> {
const enableMock = FORCE_ENABLE_MOCK || ((import.meta as any).env?.VITE_ENABLE_MOCK === 'true') return await retryRequest(() => request.post(url, data, config))
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)
}
} }
// PUT 请求 // PUT 请求

View File

@ -50,6 +50,7 @@ export interface UserProfile {
export interface LoginRequest { export interface LoginRequest {
email?: string email?: string
phone?: string phone?: string
username?: string // 新增username字段用于适配后端API
password: string password: string
captcha?: string captcha?: string
} }
@ -81,7 +82,7 @@ export interface RegisterRequest {
// 课程相关类型 // 课程相关类型
export interface Course { export interface Course {
id: number id: string // 改为string类型保持后端ID的原始格式
title: string title: string
subtitle?: string subtitle?: string
description: string description: string
@ -175,6 +176,134 @@ export interface CourseCategory {
children?: 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 { export interface Instructor {
id: number id: number
name: string name: string
@ -239,32 +368,32 @@ export interface LessonResource {
// 后端API返回的章节数据结构 // 后端API返回的章节数据结构
export interface BackendCourseSection { export interface BackendCourseSection {
id: number id: string
lessonId: number courseId: string
videoUrl: string // 视频链接
name: string // 章节名称 name: string // 章节名称
type: number | null // 章节类型0=视频、1=资料、2=考试、3=作业null=未设置
sortOrder: number // 排序 sortOrder: number // 排序
parentId: number // 父章节ID parentId: string // 父章节ID
level: number // 层级0=子级课时1=父级(章节) level: number // 章节层级0=一级章节、1=二级章节
revision: number // 版本号 createBy: string
createdBy: number createTime: string
createdTime: string | null updateBy: string
updatedBy: number updateTime: string
updatedTime: string | null
} }
// 前端使用的课程章节类型(适配后的数据结构) // 前端使用的课程章节类型(适配后的数据结构)
export interface CourseSection { export interface CourseSection {
id: number id: string // 改为string类型保持一致性
lessonId: number lessonId: string // 改为string类型与Course.id保持一致
outline: string // 章节大纲/内容链接从videoUrl适配 outline: string // 章节大纲/内容链接
name: string // 章节名称 name: string // 章节名称
parentId: number // 父章节ID type: number | null // 章节类型0=视频、1=资料、2=考试、3=作业null=未设置
parentId: string // 父章节ID改为string类型
sort: number // 排序从sortOrder适配 sort: number // 排序从sortOrder适配
level: number // 层级0=父级章节1=子级(课时)- 已从后端数据转换 level: number // 层级0=一级章节、1=二级章节
revision: number // 版本号 revision: number // 版本号
createdAt: number | null // 从createdTime适配 createdAt: number | null // 从createTime适配
updatedAt: number | null // 从updatedTime适配 updatedAt: number | null // 从updateTime适配
deletedAt: string | null deletedAt: string | null
completed?: boolean // 是否已完成(前端状态) completed?: boolean // 是否已完成(前端状态)
duration?: string // 课时时长(前端计算) duration?: string // 课时时长(前端计算)
@ -272,8 +401,78 @@ export interface CourseSection {
// 后端章节列表响应格式 // 后端章节列表响应格式
export interface BackendCourseSectionListResponse { export interface BackendCourseSectionListResponse {
list: BackendCourseSection[] success: boolean
total: number 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 // 当前选中的质量
} }
// 前端章节列表响应格式 // 前端章节列表响应格式

View File

@ -100,15 +100,21 @@ const handleLogin = async () => {
isLoading.value = true isLoading.value = true
try { try {
console.log('🚀 开始登录:', { account: loginForm.account, password: '***' }) console.log('🚀 开始登录:', { account: loginForm.account, password: '***' })
console.log('🔍 表单密码长度:', loginForm.password?.length)
console.log('🔍 表单密码内容:', loginForm.password)
// //
const isPhone = /^[0-9]+$/.test(loginForm.account) const isPhone = /^[0-9]+$/.test(loginForm.account)
// API const loginParams = {
const response = await AuthApi.login({
...(isPhone ? { phone: loginForm.account } : { email: loginForm.account }), ...(isPhone ? { phone: loginForm.account } : { email: loginForm.account }),
password: loginForm.password password: loginForm.password
}) }
console.log('🔍 准备发送的登录参数:', loginParams)
// API
const response = await AuthApi.login(loginParams)
console.log('✅ 登录响应:', response) console.log('✅ 登录响应:', response)
@ -120,6 +126,7 @@ const handleLogin = async () => {
userStore.token = token userStore.token = token
// //
localStorage.setItem('X-Access-Token', token)
localStorage.setItem('token', token) localStorage.setItem('token', token)
localStorage.setItem('refreshToken', refreshToken || '') localStorage.setItem('refreshToken', refreshToken || '')
localStorage.setItem('user', JSON.stringify(user)) localStorage.setItem('user', JSON.stringify(user))

View File

@ -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))
)
}

View File

@ -7,7 +7,7 @@ export type Course = ApiCourse
export interface Lesson { export interface Lesson {
id: number id: number
courseId: number courseId: string // 改为string类型与Course.id保持一致
title: string title: string
description: string description: string
videoUrl?: string videoUrl?: string
@ -63,13 +63,13 @@ export const useCourseStore = defineStore('course', () => {
console.log('尝试从API获取课程数据...') console.log('尝试从API获取课程数据...')
const response = await CourseApi.getCourses() const response = await CourseApi.getCourses()
console.log('API响应:', response) console.log('API响应:', response)
courses.value = response.data.list courses.value = response.data
} catch (error) { } catch (error) {
console.error('API调用失败使用模拟数据:', error) console.error('API调用失败使用模拟数据:', error)
// 如果API调用失败使用模拟数据作为后备 // 如果API调用失败使用模拟数据作为后备
const mockCourses: Course[] = [ const mockCourses: Course[] = [
{ {
id: 1, id: "1",
title: 'Vue.js 3 完整教程', title: 'Vue.js 3 完整教程',
description: '从零开始学习Vue.js 3包括Composition API、TypeScript集成等现代开发技术', description: '从零开始学习Vue.js 3包括Composition API、TypeScript集成等现代开发技术',
content: '详细的Vue.js 3课程内容', content: '详细的Vue.js 3课程内容',
@ -114,7 +114,7 @@ export const useCourseStore = defineStore('course', () => {
publishedAt: '2024-01-01' publishedAt: '2024-01-01'
}, },
{ {
id: 2, id: "2",
title: 'React 18 实战开发', title: 'React 18 实战开发',
description: '掌握React 18的新特性包括并发渲染、Suspense等高级功能', description: '掌握React 18的新特性包括并发渲染、Suspense等高级功能',
content: '详细的React 18课程内容', content: '详细的React 18课程内容',
@ -159,7 +159,7 @@ export const useCourseStore = defineStore('course', () => {
publishedAt: '2024-01-05' publishedAt: '2024-01-05'
}, },
{ {
id: 3, id: "3",
title: 'Node.js 后端开发', title: 'Node.js 后端开发',
description: '学习Node.js后端开发包括Express、数据库操作、API设计等', description: '学习Node.js后端开发包括Express、数据库操作、API设计等',
content: '详细的Node.js课程内容', 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 isLoading.value = true
try { try {
const response = await CourseApi.getCourseById(id) 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 isLoading.value = true
try { try {
// 模拟API调用 // 模拟API调用
@ -271,7 +271,7 @@ export const useCourseStore = defineStore('course', () => {
} }
} }
const enrollCourse = async (courseId: number) => { const enrollCourse = async (courseId: string) => {
isLoading.value = true isLoading.value = true
try { try {
// 模拟API调用 // 模拟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) const course = enrolledCourses.value.find(c => c.id === courseId)
if (course) { if (course) {
course.progress = progress course.progress = progress

View File

@ -8,7 +8,7 @@ export interface User extends ApiUser {}
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
// 状态 // 状态
const user = ref<User | null>(null) 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) const isLoading = ref(false)
// 计算属性 // 计算属性
@ -49,7 +49,7 @@ export const useUserStore = defineStore('user', () => {
// 无论API调用是否成功都清除本地数据 // 无论API调用是否成功都清除本地数据
user.value = null user.value = null
token.value = null token.value = null
localStorage.removeItem('token') localStorage.removeItem('X-Access-Token')
localStorage.removeItem('refreshToken') localStorage.removeItem('refreshToken')
localStorage.removeItem('user') localStorage.removeItem('user')
localStorage.removeItem('rememberMe') localStorage.removeItem('rememberMe')
@ -136,7 +136,7 @@ export const useUserStore = defineStore('user', () => {
const initializeAuth = async () => { const initializeAuth = async () => {
const savedUser = localStorage.getItem('user') const savedUser = localStorage.getItem('user')
const savedToken = localStorage.getItem('token') const savedToken = localStorage.getItem('X-Access-Token')
if (savedUser && savedToken) { if (savedUser && savedToken) {
try { try {

View File

@ -140,7 +140,15 @@
<!-- 讲师信息 --> <!-- 讲师信息 -->
<div class="instructors-section"> <div class="instructors-section">
<h3 class="section-title">讲师</h3> <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-item" v-for="instructor in instructors" :key="instructor.id">
<div class="instructor-avatar"> <div class="instructor-avatar">
<SafeAvatar :src="instructor.avatar" :name="instructor.name" :size="50" /> <SafeAvatar :src="instructor.avatar" :name="instructor.name" :size="50" />
@ -463,7 +471,7 @@ import RegisterModal from '@/components/auth/RegisterModal.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const courseId = ref(Number(route.params.id)) const courseId = ref(route.params.id as string)
const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth() const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
// enrollCourse 使 // enrollCourse 使
@ -480,6 +488,10 @@ const courseSections = ref<CourseSection[]>([])
const sectionsLoading = ref(false) const sectionsLoading = ref(false)
const sectionsError = ref('') const sectionsError = ref('')
//
const instructorsLoading = ref(false)
const instructorsError = ref('')
// //
const isEnrolled = ref(false) // const isEnrolled = ref(false) //
const enrollmentLoading = ref(false) // const enrollmentLoading = ref(false) //
@ -525,46 +537,12 @@ interface ChapterGroup {
const groupedSections = ref<ChapterGroup[]>([]) const groupedSections = ref<ChapterGroup[]>([])
// // API
const generateMockSections = (): CourseSection[] => { // const generateMockSections = (): CourseSection[] => {
return [ // 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' }
]
}
// //
const groupSectionsByChapter = (sections: CourseSection[]) => { const groupSectionsByChapter = (sections: CourseSection[]) => {
@ -603,10 +581,10 @@ const groupSectionsByChapter = (sections: CourseSection[]) => {
// console.log('') // console.log('')
// courseSections.value = generateMockSections() // courseSections.value = generateMockSections()
// } // }
//
// console.log(':', courseSections.value) // console.log(':', courseSections.value)
// console.log(':', courseSections.value.length) // console.log(':', courseSections.value.length)
//
// // 使 // // 使
// groupedSections.value = groupSectionsByChapter(courseSections.value) // groupedSections.value = groupSectionsByChapter(courseSections.value)
// console.log(':', groupedSections.value) // console.log(':', groupedSections.value)
@ -715,7 +693,7 @@ const displayComments = ref([
const loadCourseDetail = async () => { const loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value) console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value || isNaN(courseId.value)) { if (!courseId.value || courseId.value.trim() === '') {
error.value = '课程ID无效' error.value = '课程ID无效'
console.error('课程ID无效:', courseId.value) console.error('课程ID无效:', courseId.value)
return return
@ -768,7 +746,7 @@ const loadCourseDetail = async () => {
// //
const loadCourseSections = async () => { const loadCourseSections = async () => {
if (!courseId.value || isNaN(courseId.value)) { if (!courseId.value || courseId.value.trim() === '') {
sectionsError.value = '课程ID无效' sectionsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value) console.error('课程ID无效:', courseId.value)
return return
@ -783,17 +761,17 @@ const loadCourseSections = async () => {
console.log('章节API响应:', response) console.log('章节API响应:', response)
if (response.code === 0 || response.code === 200) { if (response.code === 0 || response.code === 200) {
if (response.data && Array.isArray(response.data)) { if (response.data && response.data.list && Array.isArray(response.data.list)) {
courseSections.value = response.data courseSections.value = response.data.list
groupedSections.value = groupSectionsByChapter(response.data) groupedSections.value = groupSectionsByChapter(response.data.list)
console.log('章节数据设置成功:', courseSections.value) console.log('章节数据设置成功:', courseSections.value)
console.log('分组数据:', groupedSections.value) console.log('分组数据:', groupedSections.value)
} else { } else {
console.log('API返回的章节数据为空使用模拟数据') console.log('⚠️ API返回的章节数据为空使用模拟数据')
loadMockData() loadMockData()
} }
} else { } else {
console.log('API返回错误使用模拟数据') console.log('⚠️ API返回错误使用模拟数据')
loadMockData() loadMockData()
} }
} catch (err) { } catch (err) {
@ -807,15 +785,47 @@ const loadCourseSections = async () => {
// //
const loadMockData = () => { const loadMockData = () => {
console.log('加载模拟章节数据') console.log('⚠️ API调用失败暂不使用模拟数据')
const mockSections = generateMockSections() // API
courseSections.value = mockSections courseSections.value = []
groupedSections.value = groupSectionsByChapter(mockSections) groupedSections.value = []
}
// //
// const completed = mockSections.filter(section => section.completed).length const loadCourseInstructors = async () => {
// completedLessons.value = completed if (!courseId.value || courseId.value.trim() === '') {
// progress.value = Math.round((completed / mockSections.length) * 100) 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 // section.id
const durationIndex = section.id - 1 const durationIndex = parseInt(section.id) - 1
if (durationIndex >= 0 && durationIndex < durations.length) { if (durationIndex >= 0 && durationIndex < durations.length) {
return durations[durationIndex] || '' return durations[durationIndex] || ''
} }
@ -931,19 +941,8 @@ const handleDownload = (section: CourseSection) => {
// //
const handleHomework = (section: CourseSection) => { const handleHomework = (section: CourseSection) => {
console.log('打开作业:', section) console.log('打开作业:', section)
//
// alert(`打开作业: ${section.name}`)
router.push({
name: 'Practice',
params: {
courseId: courseId.value,
sectionId: section.id
},
query: {
courseName: course.value?.title || '课程名称',
practiceName: section.name
}
})
} }
// //
@ -1142,7 +1141,8 @@ onMounted(() => {
console.log('课程详情页加载完成课程ID:', courseId.value) console.log('课程详情页加载完成课程ID:', courseId.value)
initializeMockState() // initializeMockState() //
loadCourseDetail() loadCourseDetail()
loadCourseSections() loadCourseSections() //
loadCourseInstructors() //
}) })
</script> </script>
@ -1832,6 +1832,34 @@ onMounted(() => {
color: #999; 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 { .course-info-divider {
height: 1px; height: 1px;

View File

@ -49,6 +49,29 @@
<p>请选择要播放的视频课程</p> <p>请选择要播放的视频课程</p>
</div> </div>
</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> </div>
<!-- 底部交互区域 --> <!-- 底部交互区域 -->
@ -377,7 +400,7 @@ import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { CourseApi } from '@/api/modules/course' 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 SafeAvatar from '@/components/common/SafeAvatar.vue'
import LearningProgressStats from '@/components/common/LearningProgressStats.vue' import LearningProgressStats from '@/components/common/LearningProgressStats.vue'
import NotesModal from '@/components/common/NotesModal.vue' import NotesModal from '@/components/common/NotesModal.vue'
@ -395,7 +418,7 @@ declare global {
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const courseId = ref(Number(route.params.id)) const courseId = ref(route.params.id as string)
// false // false
const FORCE_LOCAL_VIDEO = true const FORCE_LOCAL_VIDEO = true
@ -405,6 +428,13 @@ const currentSection = ref<CourseSection | null>(null)
const currentVideoUrl = ref<string>('') const currentVideoUrl = ref<string>('')
const ckplayer = ref<any>(null) 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 = { const VIDEO_CONFIG = {
// 使 // 使
@ -534,74 +564,50 @@ const displayComments = ref([
} }
]) ])
// //
const generateMockSections = (): CourseSection[] => { const generateMockSections = (): CourseSection[] => {
return [ // API
// - (4) return []
{ 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' }
]
} }
// // -
const groupSectionsByChapter = (sections: CourseSection[]) => { const groupSectionsByChapter = (sections: CourseSection[]) => {
const chapterTitles = [ console.log('🔍 开始分组章节数据:', sections)
'课前准备',
'程序设计基础知识',
'实战项目',
'高级应用',
'拓展学习',
'答疑与交流'
]
const groups: ChapterGroup[] = [] const groups: ChapterGroup[] = []
let sectionsPerChapter = [4, 5, 6, 4, 3, 2] //
let sectionIndex = 0
for (let i = 0; i < chapterTitles.length; i++) { // level=1
const chapterSections = sections.slice(sectionIndex, sectionIndex + sectionsPerChapter[i]) const parentChapters = sections.filter(section => section.level === 1)
if (chapterSections.length > 0) { console.log('🔍 找到一级章节:', parentChapters)
groups.push({
title: chapterTitles[i], //
sections: chapterSections, parentChapters.forEach((parentChapter, index) => {
expanded: i === 0 // // level=2parentId
}) const childSections = sections.filter(section =>
} section.level === 2 && section.parentId === parentChapter.id
sectionIndex += sectionsPerChapter[i] )
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 return groups
} }
@ -609,7 +615,7 @@ const groupSectionsByChapter = (sections: CourseSection[]) => {
const loadCourseDetail = async () => { const loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value) console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value || isNaN(courseId.value)) { if (!courseId.value || courseId.value.trim() === '') {
error.value = '课程ID无效' error.value = '课程ID无效'
console.error('课程ID无效:', courseId.value) console.error('课程ID无效:', courseId.value)
return return
@ -640,7 +646,7 @@ const loadCourseDetail = async () => {
// //
const loadCourseSections = async () => { const loadCourseSections = async () => {
if (!courseId.value || isNaN(courseId.value)) { if (!courseId.value || courseId.value.trim() === '') {
sectionsError.value = '课程ID无效' sectionsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value) console.error('课程ID无效:', courseId.value)
return return
@ -655,11 +661,11 @@ const loadCourseSections = async () => {
console.log('章节API响应:', response) console.log('章节API响应:', response)
if (response.code === 0 || response.code === 200) { if (response.code === 0 || response.code === 200) {
if (response.data && Array.isArray(response.data)) { if (response.data && response.data.list && Array.isArray(response.data.list)) {
courseSections.value = response.data courseSections.value = response.data.list
groupedSections.value = groupSectionsByChapter(response.data) groupedSections.value = groupSectionsByChapter(response.data.list)
console.log('章节数据设置成功:', courseSections.value) console.log('章节数据设置成功:', courseSections.value)
console.log('分组数据:', groupedSections.value) console.log('分组数据:', groupedSections.value)
// 使 // 使
if (!FORCE_LOCAL_VIDEO) { if (!FORCE_LOCAL_VIDEO) {
const firstVideo = courseSections.value.find(s => s.outline && (s.outline.includes('.m3u8') || s.outline.includes('.mp4'))) 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 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 getChapterNumber = (num: number) => {
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'] const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
@ -729,19 +832,39 @@ const getChapterNumber = (num: number) => {
// //
const isVideoLesson = (section: CourseSection) => { const isVideoLesson = (section: CourseSection) => {
console.log(section.outline) console.log('检查章节类型:', section.name, 'type:', section.type, 'outline:', section.outline)
// type0=
if (section.type === 0) {
return true
}
// typenulloutline
return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4')) return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4'))
} }
const isResourceLesson = (section: CourseSection) => { const isResourceLesson = (section: CourseSection) => {
// type1=
if (section.type === 1) {
return true
}
// typenulloutline
return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip')) return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip'))
} }
const isHomeworkLesson = (section: CourseSection) => { const isHomeworkLesson = (section: CourseSection) => {
// type3=
if (section.type === 3) {
return true
}
// typenull
return section.name.includes('作业') || section.name.includes('练习') return section.name.includes('作业') || section.name.includes('练习')
} }
const isExamLesson = (section: CourseSection) => { const isExamLesson = (section: CourseSection) => {
// type2=
if (section.type === 2) {
return true
}
// typenull
return section.name.includes('考试') || section.name.includes('测试') return section.name.includes('考试') || section.name.includes('测试')
} }
@ -771,18 +894,39 @@ const formatLessonDuration = (section: CourseSection) => {
// - // -
const handleSectionClick = (section: CourseSection) => { const handleSectionClick = (section: CourseSection) => {
console.log('点击课程章节:', section.name) console.log('🔍 点击课程章节:', section.name, section)
currentSection.value = section currentSection.value = section
// //
if (isVideoLesson(section)) { const isVideo = isVideoLesson(section)
handleVideoPlay(section) const isResource = isResourceLesson(section)
} else if (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) handleDownload(section)
} else if (isHomeworkLesson(section)) { } else if (isHomework) {
console.log('✅ 识别为作业课程')
handleHomework(section) handleHomework(section)
} else if (isExamLesson(section)) { } else if (isExam) {
console.log('✅ 识别为考试课程')
handleExam(section) handleExam(section)
} else {
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
loadSectionVideo(section)
} }
} }
@ -1158,6 +1302,73 @@ onUnmounted(() => {
opacity: 0.9; 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 { .course-info-section {
/* padding: 24px 0; */ /* padding: 24px 0; */

View File

@ -368,7 +368,7 @@ import type { Course, CourseSection } from '@/api/types'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const courseId = ref(Number(route.params.id)) const courseId = ref(route.params.id as string)
// URL // URL
const currentVideoUrl = ref(route.query.videoUrl ? decodeURIComponent(route.query.videoUrl as string) : '') 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 course = ref<Course | null>(null)
const courseSections = ref<CourseSection[]>([]) const courseSections = ref<CourseSection[]>([])
const currentLessonId = ref(currentSectionId.value || 1) const currentLessonId = ref(currentSectionId.value ? currentSectionId.value.toString() : "1")
// //
const isPlaying = ref(false) const isPlaying = ref(false)
@ -476,12 +476,12 @@ const sortedChapters = computed(() => {
const hasPrevious = computed(() => { const hasPrevious = computed(() => {
// //
return currentLessonId.value > 1 return parseInt(currentLessonId.value) > 1
}) })
const hasNext = computed(() => { const hasNext = computed(() => {
// //
return currentLessonId.value < totalLessons.value return parseInt(currentLessonId.value) < totalLessons.value
}) })
const isCurrentLessonCompleted = computed(() => { const isCurrentLessonCompleted = computed(() => {
@ -498,7 +498,7 @@ watch(() => route.query, (newQuery) => {
currentVideoTitle.value = decodeURIComponent(newQuery.sectionName as string) currentVideoTitle.value = decodeURIComponent(newQuery.sectionName as string)
} }
if (newQuery.sectionId) { if (newQuery.sectionId) {
currentLessonId.value = Number(newQuery.sectionId) currentLessonId.value = newQuery.sectionId.toString()
} }
}) })
@ -537,18 +537,19 @@ const generateChapterGroups = () => {
title: '课前准备', title: '课前准备',
lessons: [ lessons: [
{ {
id: 1, id: "1",
lessonId: courseId.value, lessonId: courseId.value,
outline: currentVideoUrl.value, outline: currentVideoUrl.value,
name: currentVideoTitle.value || '开课彩蛋:新开始新征程', name: currentVideoTitle.value || '开课彩蛋:新开始新征程',
parentId: 1, parentId: "1",
sort: 0, sort: 0,
level: 1, level: 1,
revision: 0, revision: 0,
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: null, updatedAt: null,
deletedAt: null, deletedAt: null,
completed: false completed: false,
type: 0
} }
], ],
expanded: true expanded: true

View File

@ -17,70 +17,38 @@
<div class="filter-group"> <div class="filter-group">
<span class="filter-label">类型</span> <span class="filter-label">类型</span>
<div class="filter-tags"> <div class="filter-tags">
<span class="filter-tag" :class="{ active: selectedSubject === '全部' }" <span class="filter-tag" :class="{ active: selectedMajor === '全部' }"
@click="selectSubject('全部')">全部</span> @click="selectMajor('全部')">全部</span>
<span class="filter-tag" :class="{ active: selectedSubject === '必修课' }" <span
@click="selectSubject('必修课')">必修课</span> v-for="category in categories"
<span class="filter-tag" :class="{ active: selectedSubject === '高分课' }" :key="category.id"
@click="selectSubject('高分课')">高分课</span> class="filter-tag"
<span class="filter-tag" :class="{ active: selectedSubject === '名师课堂' }" :class="{ active: selectedMajor === category.name }"
@click="selectSubject('名师课堂')">名师课堂</span> @click="selectMajor(category.name)"
<span class="filter-tag" :class="{ active: selectedSubject === '训练营' }" >
@click="selectSubject('训练营')">训练营</span> {{ category.name }}
<span class="filter-tag" :class="{ active: selectedSubject === '无考试' }" </span>
@click="selectSubject('无考试')">无考试</span> <!-- 加载状态 -->
<span class="filter-tag" :class="{ active: selectedSubject === '专题讲座' }" <span v-if="categoriesLoading" class="filter-tag loading">加载中...</span>
@click="selectSubject('专题讲座')">专题讲座</span>
</div> </div>
</div> </div>
<!-- 专题分类第一行 --> <!-- 专题分类 -->
<div class="filter-group"> <div class="filter-group">
<span class="filter-label">专题</span> <span class="filter-label">专题</span>
<div class="filter-tags"> <div class="filter-tags">
<span class="filter-tag" :class="{ active: selectedMajor === '全部' }" @click="selectMajor('全部')">全部</span> <span class="filter-tag" :class="{ active: selectedSubject === '全部' }" @click="selectSubject('全部')">全部</span>
<span class="filter-tag" :class="{ active: selectedMajor === '学科教研' }" <span
@click="selectMajor('学科教研')">学科教研</span> v-for="subject in subjects"
<span class="filter-tag" :class="{ active: selectedMajor === '班级管理' }" :key="subject.id"
@click="selectMajor('班级管理')">班级管理</span> class="filter-tag"
<span class="filter-tag" :class="{ active: selectedMajor === '通识技能' }" :class="{ active: selectedSubject === subject.name }"
@click="selectMajor('通识技能')">通识技能</span> @click="selectSubject(subject.name)"
<span class="filter-tag" :class="{ active: selectedMajor === '信息素养' }" >
@click="selectMajor('信息素养')">信息素养</span> {{ subject.name }}
<span class="filter-tag" :class="{ active: selectedMajor === '师风师德' }" </span>
@click="selectMajor('师风师德')">师风师德</span> <!-- 加载状态 -->
<span class="filter-tag" :class="{ active: selectedMajor === '专题教育' }" <span v-if="subjectsLoading" class="filter-tag loading">加载中...</span>
@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>
</div> </div>
</div> </div>
@ -91,14 +59,17 @@
<div class="filter-tags"> <div class="filter-tags">
<span class="filter-tag" :class="{ active: selectedDifficulty === '全部' }" <span class="filter-tag" :class="{ active: selectedDifficulty === '全部' }"
@click="selectDifficulty('全部')">全部</span> @click="selectDifficulty('全部')">全部</span>
<span class="filter-tag" :class="{ active: selectedDifficulty === '零基础' }" <span
@click="selectDifficulty('零基础')">零基础</span> v-for="difficulty in difficulties"
<span class="filter-tag" :class="{ active: selectedDifficulty === '初级' }" :key="difficulty.id"
@click="selectDifficulty('初级')">初级</span> class="filter-tag"
<span class="filter-tag" :class="{ active: selectedDifficulty === '中级' }" :class="{ active: selectedDifficulty === difficulty.name }"
@click="selectDifficulty('中级')">中级</span> @click="selectDifficulty(difficulty.name)"
<span class="filter-tag" :class="{ active: selectedDifficulty === '高级' }" >
@click="selectDifficulty('高级')">高级</span> {{ difficulty.name }}
</span>
<!-- 加载状态 -->
<span v-if="difficultiesLoading" class="filter-tag loading">加载中...</span>
</div> </div>
</div> </div>
</div> </div>
@ -118,9 +89,9 @@
<!-- 排序标签 --> <!-- 排序标签 -->
<div class="sort-tabs"> <div class="sort-tabs">
<span class="sort-tab" :class="{ active: selectedSort === '最新' }" @click="selectSort('最新')">最新</span> <span class="sort-tab">最新</span>
<span class="sort-tab" :class="{ active: selectedSort === '最热' }" @click="selectSort('最热')">最热</span> <span class="sort-tab">最热</span>
<span class="sort-tab" :class="{ active: selectedSort === '推荐' }" @click="selectSort('推荐')">推荐</span> <span class="sort-tab active">推荐</span>
</div> </div>
<!-- 加载状态 --> <!-- 加载状态 -->
@ -203,8 +174,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { Course } from '@/api/types' import type { Course, CourseCategory, CourseSubject, CourseDifficulty } from '@/api/types'
import { mockCourses } from '@/data/mockCourses' import { CourseApi } from '@/api'
const router = useRouter() const router = useRouter()
@ -213,6 +184,18 @@ const courses = ref<Course[]>([])
const loading = ref(false) const loading = ref(false)
const total = ref(0) 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 selectedSubject = ref('全部')
const selectedMajor = ref('全部') const selectedMajor = ref('全部')
@ -224,9 +207,6 @@ const itemsPerPage = 20
const totalItems = computed(() => total.value) const totalItems = computed(() => total.value)
const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage)) const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage))
//
const selectedSort = ref('推荐')
// 广 // 广
const showAdvertisement = ref(true) const showAdvertisement = ref(true)
@ -276,84 +256,59 @@ const visiblePages = computed(() => {
return pages return pages
}) })
// // 使API
const selectSort = (sort: string) => {
selectedSort.value = sort
currentPage.value = 1 //
loadCourses()
}
// 使
const loadCourses = async () => { const loadCourses = async () => {
try { try {
loading.value = true loading.value = true
console.log('🚀 加载课程数据...')
// //
await new Promise(resolve => setTimeout(resolve, 500)) const queryParams: any = {}
// // categoryId{id, name}id
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
}
//
if (selectedMajor.value !== '全部') { if (selectedMajor.value !== '全部') {
filteredCourses = filteredCourses.filter(course => const selectedCategory = categories.value.find(cat => cat.name === selectedMajor.value)
course.title.includes(selectedMajor.value) || if (selectedCategory) {
course.description.includes(selectedMajor.value) || queryParams.categoryId = selectedCategory.id.toString()
course.tags.some(tag => tag.includes(selectedMajor.value)) console.log('🏷️ 选择的分类:', selectedCategory.name, 'ID:', selectedCategory.id)
) }
} }
// // difficulty{value, label}value
if (selectedDifficulty.value !== '全部') { if (selectedDifficulty.value !== '全部') {
const difficultyMap: { [key: string]: string } = { const selectedDiff = difficulties.value.find(diff => diff.name === selectedDifficulty.value)
'初级': 'beginner', if (selectedDiff) {
'中级': 'intermediate', queryParams.difficulty = selectedDiff.id // 使toString()
'高级': 'advanced' console.log('📊 选择的难度:', selectedDiff.name, 'Value:', selectedDiff.id)
}
const targetLevel = difficultyMap[selectedDifficulty.value]
if (targetLevel) {
filteredCourses = filteredCourses.filter(course => course.level === targetLevel)
} }
} }
// // subject{value, label}value
total.value = filteredCourses.length if (selectedSubject.value !== '全部') {
const startIndex = (currentPage.value - 1) * itemsPerPage const selectedSubj = subjects.value.find(subj => subj.name === selectedSubject.value)
const endIndex = startIndex + itemsPerPage if (selectedSubj) {
courses.value = filteredCourses.slice(startIndex, endIndex) 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) { } catch (error) {
console.error('加载课程失败:', error) console.error('加载课程失败:', error)
courses.value = [] courses.value = []
total.value = 0 total.value = 0
} finally { } 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(() => { onMounted(() => {
loadCourses() loadCourses()
loadCategories()
loadSubjects()
loadDifficulties()
}) })
</script> </script>
@ -617,6 +641,13 @@ onMounted(() => {
font-weight: 500; font-weight: 500;
} }
.filter-tag.loading {
background: #f0f0f0;
color: #999;
cursor: not-allowed;
opacity: 0.7;
}
.sort-tabs { .sort-tabs {
display: flex; display: flex;
gap: 20px; gap: 20px;

View File

@ -528,7 +528,7 @@ import { useCourseStore } from '@/stores/course'
import { useAuth } from '@/composables/useAuth' import { useAuth } from '@/composables/useAuth'
import LoginModal from '@/components/auth/LoginModal.vue' import LoginModal from '@/components/auth/LoginModal.vue'
import RegisterModal from '@/components/auth/RegisterModal.vue' import RegisterModal from '@/components/auth/RegisterModal.vue'
import { getPopularCourses } from '@/data/mockCourses' // import { getPopularCourses } from '@/data/mockCourses'
const { t, locale } = useI18n() const { t, locale } = useI18n()
const router = useRouter() const router = useRouter()
@ -554,8 +554,8 @@ const bannerAlt = computed(() => {
// //
const popularCourses = computed(() => { const popularCourses = computed(() => {
const courses = getPopularCourses() const courses = courseStore.courses.slice(0, 4) // 4
return courses.map(course => ({ return courses.map((course: any) => ({
id: course.id, id: course.id,
title: course.title, title: course.title,
thumbnail: course.thumbnail, thumbnail: course.thumbnail,
@ -719,12 +719,12 @@ const partners = computed(() => [
// ]) // ])
// //
const goToCourseDetail = (courseId: number) => { const goToCourseDetail = (courseId: string) => {
router.push(`/course/${courseId}`) router.push(`/course/${courseId}`)
} }
// - // -
const handleEnrollCourse = (courseId: number) => { const handleEnrollCourse = (courseId: string) => {
// //
router.push(`/course/${courseId}`) router.push(`/course/${courseId}`)
} }

View File

@ -155,7 +155,7 @@ const isPlaying = ref(false)
const noteContent = ref('') const noteContent = ref('')
const progress = ref(0) 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 course = computed(() => courseStore.currentCourse)
const lessons = computed(() => courseStore.lessons) const lessons = computed(() => courseStore.lessons)

View File

@ -128,7 +128,7 @@ const toggleFollow = () => {
} }
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import type { Course } from '@/api/types' import type { Course } from '@/api/types'
import { mockCourses } from '@/data/mockCourses' // import { mockCourses } from '@/data/mockCourses'
const router = useRouter() const router = useRouter()
@ -201,7 +201,7 @@ const loadCourses = async () => {
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise(resolve => setTimeout(resolve, 500))
// //
let filteredCourses = [...mockCourses] let filteredCourses: Course[] = [] // 使API
// //
if (selectedSubject.value !== '全部') { if (selectedSubject.value !== '全部') {
@ -234,7 +234,7 @@ const loadCourses = async () => {
}); });
} else { } else {
// ID // 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 => filteredCourses = filteredCourses.filter(course =>
course.title.includes(selectedMajor.value) || course.title.includes(selectedMajor.value) ||
course.description.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))
) )
} }

View File

@ -52,7 +52,7 @@ import { ref } from 'vue'
import { CourseApi } from '@/api/modules/course' import { CourseApi } from '@/api/modules/course'
import type { CourseSection } from '@/api/types' import type { CourseSection } from '@/api/types'
const testLessonId = ref(1) const testLessonId = ref("1")
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const sections = ref<CourseSection[]>([]) const sections = ref<CourseSection[]>([])

View File

@ -1,35 +1,27 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig({
const env = loadEnv(mode, process.cwd(), '') plugins: [
const proxyTarget = env.VITE_PROXY_TARGET || 'http://110.42.96.65:55510' vue(),
vueDevTools(),
return { ],
plugins: [ resolve: {
vue(), alias: {
vueDevTools(), '@': fileURLToPath(new URL('./src', import.meta.url))
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
}, },
server: { },
port: 3000, server: {
open: true, port: 3000,
proxy: { open: true,
// 将以 /api 开头的请求代理到后端避免浏览器CORS限制 proxy: {
'/api': { '/jeecgboot': {
target: proxyTarget, target: 'http://103.40.14.23:25526',
changeOrigin: true, changeOrigin: true
// 如果后端接口不是以 /api 开头,可在这里改写路径
// rewrite: (path) => path.replace(/^\/api/, '')
}
} }
} }
} }