feat:对接环境配置,登录,课程相关接口对接
This commit is contained in:
parent
a428d2b36b
commit
13113e1bbc
2
.env
2
.env
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,9 +52,19 @@ 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: {
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户资料
|
// 更新用户资料
|
||||||
|
@ -5,6 +5,19 @@ import type {
|
|||||||
PaginationResponse,
|
PaginationResponse,
|
||||||
Course,
|
Course,
|
||||||
CourseCategory,
|
CourseCategory,
|
||||||
|
BackendCourseCategory,
|
||||||
|
BackendCourseCategoryListResponse,
|
||||||
|
CourseSubject,
|
||||||
|
BackendCourseSubject,
|
||||||
|
BackendCourseSubjectListResponse,
|
||||||
|
CourseDifficulty,
|
||||||
|
BackendCourseDifficulty,
|
||||||
|
BackendCourseDifficultyListResponse,
|
||||||
|
CourseListQueryParams,
|
||||||
|
CourseDetailQueryParams,
|
||||||
|
BackendCourseItem,
|
||||||
|
BackendCourseListResponse,
|
||||||
|
BackendCourseDetailResponse,
|
||||||
Chapter,
|
Chapter,
|
||||||
Lesson,
|
Lesson,
|
||||||
LessonResource,
|
LessonResource,
|
||||||
@ -17,7 +30,6 @@ import type {
|
|||||||
SearchRequest,
|
SearchRequest,
|
||||||
Instructor,
|
Instructor,
|
||||||
BackendCourse,
|
BackendCourse,
|
||||||
BackendCourseListResponse,
|
|
||||||
CourseListRequest,
|
CourseListRequest,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
|
||||||
@ -76,128 +88,96 @@ export class CourseApi {
|
|||||||
return '待定'
|
return '待定'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射后端难度值到前端级别
|
||||||
|
*/
|
||||||
|
private static mapDifficultyToLevel(difficulty: number): string {
|
||||||
|
switch (difficulty) {
|
||||||
|
case 0: return '零基础'
|
||||||
|
case 1: return '初级'
|
||||||
|
case 2: return '进阶'
|
||||||
|
case 3: return '高阶'
|
||||||
|
default: return '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射后端难度值到标准级别
|
||||||
|
*/
|
||||||
|
private static mapDifficultyToStandardLevel(difficulty: number): 'beginner' | 'intermediate' | 'advanced' {
|
||||||
|
switch (difficulty) {
|
||||||
|
case 0: return 'beginner'
|
||||||
|
case 1: return 'beginner'
|
||||||
|
case 2: return 'intermediate'
|
||||||
|
case 3: return 'advanced'
|
||||||
|
default: return 'beginner'
|
||||||
|
}
|
||||||
|
}
|
||||||
// 获取课程列表 - 适配后端接口
|
// 获取课程列表 - 适配后端接口
|
||||||
static async getCourses(params?: CourseListRequest): Promise<ApiResponse<PaginationResponse<Course>>> {
|
static async getCourses(params?: CourseListQueryParams): Promise<ApiResponse<Course[]>> {
|
||||||
try {
|
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)
|
||||||
|
|
||||||
@ -225,6 +205,95 @@ export class CourseApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取课程详情 - 适配后端接口
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索课程
|
// 搜索课程
|
||||||
static searchCourses(params: SearchRequest): Promise<ApiResponse<PaginationResponse<Course>>> {
|
static searchCourses(params: SearchRequest): Promise<ApiResponse<PaginationResponse<Course>>> {
|
||||||
return ApiRequest.get('/courses/search', params)
|
return ApiRequest.get('/courses/search', params)
|
||||||
@ -246,86 +315,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 +427,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: []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取分类下的课程
|
// 获取分类下的课程
|
||||||
|
@ -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 请求
|
||||||
|
129
src/api/types.ts
129
src/api/types.ts
@ -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,132 @@ 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 // 专题值
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端课程数据格式
|
||||||
|
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
|
||||||
|
@ -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))
|
||||||
|
@ -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 {
|
||||||
|
@ -463,7 +463,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 暂时未使用,后续需要时再启用
|
||||||
|
|
||||||
@ -715,7 +715,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 +768,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
|
||||||
@ -1131,7 +1131,7 @@ onMounted(() => {
|
|||||||
console.log('课程详情页加载完成,课程ID:', courseId.value)
|
console.log('课程详情页加载完成,课程ID:', courseId.value)
|
||||||
initializeMockState() // 初始化模拟状态
|
initializeMockState() // 初始化模拟状态
|
||||||
loadCourseDetail()
|
loadCourseDetail()
|
||||||
loadCourseSections()
|
// loadCourseSections() // 暂时禁用章节接口调用,因为接口不存在
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -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,9 @@
|
|||||||
<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, CourseListQueryParams } from '@/api/types'
|
||||||
import { mockCourses } from '@/data/mockCourses'
|
import { mockCourses } from '@/data/mockCourses'
|
||||||
|
import { CourseApi } from '@/api'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@ -213,6 +185,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 +208,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 +257,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.studentCount - a.studentCount)
|
|
||||||
break
|
|
||||||
case '推荐':
|
|
||||||
default:
|
|
||||||
// 推荐排序可以根据评分和学生数综合排序
|
|
||||||
filteredCourses.sort((a, b) => (b.rating * 0.7 + b.studentCount * 0.3) - (a.rating * 0.7 + a.studentCount * 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 +398,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 +642,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;
|
||||||
|
@ -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/, '')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user