feat:对接环境配置,登录,课程相关接口对接

This commit is contained in:
小张 2025-08-15 13:21:10 +08:00
parent a428d2b36b
commit 13113e1bbc
13 changed files with 775 additions and 640 deletions

2
.env
View File

@ -1,5 +1,5 @@
# API配置
VITE_API_BASE_URL=http://110.42.96.65:55510/api
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
# Mock配置 - 禁用Mock使用真实API
VITE_ENABLE_MOCK=false

View File

@ -1,7 +1,7 @@
# 开发环境配置
# API配置
VITE_API_BASE_URL=http://110.42.96.65:55510/api
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
# Mock配置
# 设置为 true 使用Mock数据false 使用真实API

View File

@ -1,7 +1,7 @@
# 生产环境配置
# API配置
VITE_API_BASE_URL=http://110.42.96.65:55510/api
VITE_API_BASE_URL=http://103.40.14.23:25526/jeecgboot
# Mock配置 - 生产环境禁用Mock使用真实API
VITE_ENABLE_MOCK=false

View File

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

View File

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

View File

@ -5,6 +5,19 @@ import type {
PaginationResponse,
Course,
CourseCategory,
BackendCourseCategory,
BackendCourseCategoryListResponse,
CourseSubject,
BackendCourseSubject,
BackendCourseSubjectListResponse,
CourseDifficulty,
BackendCourseDifficulty,
BackendCourseDifficultyListResponse,
CourseListQueryParams,
CourseDetailQueryParams,
BackendCourseItem,
BackendCourseListResponse,
BackendCourseDetailResponse,
Chapter,
Lesson,
LessonResource,
@ -17,7 +30,6 @@ import type {
SearchRequest,
Instructor,
BackendCourse,
BackendCourseListResponse,
CourseListRequest,
} from '../types'
@ -76,128 +88,96 @@ export class CourseApi {
return '待定'
}
}
/**
*
*/
private static mapDifficultyToLevel(difficulty: number): string {
switch (difficulty) {
case 0: return '零基础'
case 1: return '初级'
case 2: return '进阶'
case 3: return '高阶'
default: return '未知'
}
}
/**
*
*/
private static mapDifficultyToStandardLevel(difficulty: number): 'beginner' | 'intermediate' | 'advanced' {
switch (difficulty) {
case 0: return 'beginner'
case 1: return 'beginner'
case 2: return 'intermediate'
case 3: return 'advanced'
default: return 'beginner'
}
}
// 获取课程列表 - 适配后端接口
static async getCourses(params?: CourseListRequest): Promise<ApiResponse<PaginationResponse<Course>>> {
static async getCourses(params?: CourseListQueryParams): Promise<ApiResponse<Course[]>> {
try {
console.log('调用课程列表API参数:', params)
console.log('🚀 调用课程列表API参数:', params)
// 构建查询参数根据API文档的参数名称
const queryParams: any = {}
if (params?.categoryId) queryParams.categoryId = params.categoryId
if (params?.difficulty !== undefined) queryParams.difficulty = params.difficulty
if (params?.difficulty) queryParams.difficulty = params.difficulty
if (params?.subject) queryParams.subject = params.subject
if (params?.page) queryParams.page = params.page
if (params?.pageSize) queryParams.pageSize = params.pageSize
if (params?.keyword) queryParams.keyword = params.keyword
console.log('🔍 查询参数:', queryParams)
// 调用后端API
const response = await ApiRequest.get<BackendCourseListResponse>('/lesson/list', queryParams)
console.log('课程列表API响应:', response)
console.log('响应数据结构:', response.data)
console.log('响应数据类型:', typeof response.data)
const response = await ApiRequest.get<any>('/biz/course/list', queryParams)
console.log('🔍 课程列表API响应:', response)
// 检查是否是axios响应格式还是我们的ApiResponse格式
let actualData: any
let actualCode: number
let actualMessage: string
// 使用类型断言来处理不同的响应格式
const responseAny = response as any
if (responseAny.data && typeof responseAny.data === 'object' && 'data' in responseAny.data) {
// 这是我们期望的ApiResponse格式: { code, message, data: { list, total } }
actualData = responseAny.data.data
actualCode = responseAny.data.code
actualMessage = responseAny.data.message
console.log('检测到ApiResponse格式')
} else {
// 这可能是直接的axios响应格式: { list, total }
actualData = responseAny.data
actualCode = responseAny.status || 200
actualMessage = responseAny.statusText || 'OK'
console.log('检测到直接响应格式')
}
console.log('实际数据:', actualData)
console.log('实际数据的list字段:', actualData?.list)
console.log('list是否为数组:', Array.isArray(actualData?.list))
// 检查响应数据的有效性
if (!actualData || !Array.isArray(actualData.list)) {
console.warn('API响应数据格式异常')
console.warn('期望的格式: { list: [], total: number }')
console.warn('实际收到的格式:', actualData)
return {
code: actualCode || 0,
message: actualMessage || '数据格式异常',
data: {
list: [],
total: 0,
page: params?.page || 1,
pageSize: params?.pageSize || 10,
totalPages: 0
}
}
}
// 适配后端响应格式为前端期望的格式
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)
// 处理后端响应格式
if (response.data && response.data.success && response.data.result) {
// 转换后端数据格式为前端格式
const courses: Course[] = response.data.result.map((item: BackendCourseItem) => ({
id: item.id, // 保持字符串格式,不转换为数字
title: item.name || '',
description: item.description || '',
instructor: item.school || '未知讲师',
duration: item.arrangement || '待定',
level: this.mapDifficultyToLevel(item.difficulty),
category: item.subject || '其他',
thumbnail: item.cover || '',
price: 0, // 后端没有价格字段设为0
rating: 0, // 后端没有评分字段设为0
studentsCount: item.enrollCount || 0,
lessonsCount: 0, // 后端没有课程数量字段设为0
tags: [],
isEnrolled: false,
progress: 0,
createdAt: this.formatTimestamp(item.createTime),
updatedAt: this.formatTimestamp(item.updateTime),
status: item.status === 1 ? 'published' : 'draft',
enrollmentCount: item.enrollCount || 0,
maxEnrollment: item.maxEnroll || 0,
startDate: item.startTime || '',
endDate: item.endTime || '',
outline: item.outline || '',
prerequisite: item.prerequisite || '',
reference: item.reference || '',
target: item.target || '',
question: item.question || '',
video: item.video || ''
}))
const 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 {
code: 200,
message: '获取成功',
data: courses
}
} else {
console.warn('⚠️ 课程列表API返回格式异常:', response)
return {
code: 500,
message: response.data?.message || '获取课程列表失败',
data: []
}
}
return adaptedResponse
} catch (error: any) {
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>>> {
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 {
// 调用后端课程详情接口
const response = await ApiRequest.get<BackendCourse>('/lesson/detail', { id })
const response = await ApiRequest.get<any>('/biz/course/detail', { id })
// 检查是否是axios响应格式还是我们的ApiResponse格式
let actualData: any
let actualCode: number
let actualMessage: string
console.log('🔍 课程详情API响应:', response)
// 使用类型断言来处理不同的响应格式
const responseAny = response as any
if (responseAny.data && typeof responseAny.data === 'object' && 'data' in responseAny.data) {
// 这是我们期望的ApiResponse格式: { code, message, data: BackendCourse }
actualData = responseAny.data.data
actualCode = responseAny.data.code
actualMessage = responseAny.data.message
console.log('检测到ApiResponse格式')
} else {
// 这可能是直接的axios响应格式: BackendCourse
actualData = responseAny.data
actualCode = responseAny.status || 200
actualMessage = responseAny.statusText || 'OK'
console.log('检测到直接响应格式')
// 处理后端响应格式
if (response.data && response.data.success) {
// 检查result是否为null或空
if (!response.data.result) {
console.warn('⚠️ 课程详情为空,可能课程不存在或已删除')
return {
code: 404,
message: '课程不存在或已删除',
data: {} as Course
}
// 适配数据格式
const adaptedCourse: Course = {
id: actualData.id,
title: actualData.name,
description: actualData.description,
content: actualData.outline, // 使用 outline 作为课程内容
thumbnail: actualData.cover,
coverImage: actualData.cover,
price: parseFloat(actualData.price || '0'),
originalPrice: parseFloat(actualData.price || '0'),
}
// 转换后端数据格式为前端格式
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: 4.5,
rating: 0, // 后端没有评分字段设为0
ratingCount: 0,
studentsCount: 0,
duration: this.calculateDuration(actualData.startTime, actualData.endTime),
studentsCount: item.enrollCount || 0,
duration: item.arrangement || '待定',
totalLessons: 0,
level: 'beginner' as const,
level: this.mapDifficultyToStandardLevel(item.difficulty),
language: 'zh-CN',
category: {
id: actualData.categoryId,
name: '未分类',
slug: 'uncategorized'
id: 1,
name: item.subject || '其他',
slug: 'other'
},
tags: [],
skills: [],
requirements: actualData.prerequisite ? [actualData.prerequisite] : [],
objectives: actualData.target ? [actualData.target] : [],
requirements: item.prerequisite ? [item.prerequisite] : [],
objectives: item.target ? [item.target] : [],
instructor: {
id: actualData.teacherId || 0,
name: actualData.school || '未知讲师',
id: 1,
name: item.school || '未知讲师',
title: '讲师',
bio: actualData.position || '',
bio: '',
avatar: '',
rating: 4.5,
studentsCount: 0,
coursesCount: 0,
experience: actualData.arrangement || '',
experience: '',
education: [],
certifications: []
},
status: 'published' as const,
createdAt: this.formatTimestamp(actualData.createdTime),
updatedAt: this.formatTimestamp(actualData.updatedTime),
publishedAt: actualData.startTime
status: item.status === 1 ? 'published' : 'draft',
isEnrolled: false,
progress: 0,
createdAt: this.formatTimestamp(item.createTime),
updatedAt: this.formatTimestamp(item.updateTime)
}
return {
code: actualCode,
message: actualMessage,
data: adaptedCourse
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
}
} catch (error) {
throw error
}
}
@ -355,8 +427,135 @@ export class CourseApi {
}
// 获取课程分类
static getCategories(): Promise<ApiResponse<CourseCategory[]>> {
return ApiRequest.get('/categories')
static async getCategories(): Promise<ApiResponse<CourseCategory[]>> {
try {
console.log('🚀 获取课程分类列表')
// 调用后端API不需要token
const response = await ApiRequest.get<any>('/biz/course/category/list')
console.log('🔍 分类API响应:', response)
// 处理后端响应格式
if (response.data && response.data.success && response.data.result) {
// 转换后端数据格式为前端格式
const categories: CourseCategory[] = response.data.result.map((item: any) => ({
id: parseInt(item.id) || 0,
name: item.name || '',
slug: item.name?.toLowerCase().replace(/\s+/g, '-') || '',
description: '',
sortOrder: item.sortOrder || 0
}))
return {
code: 200,
message: '获取成功',
data: categories
}
} else {
console.warn('⚠️ 分类API返回格式异常:', response)
return {
code: 500,
message: response.data?.message || '获取分类失败',
data: []
}
}
} catch (error: any) {
console.error('❌ 获取课程分类失败:', error)
return {
code: 500,
message: error.message || '获取分类失败',
data: []
}
}
}
// 获取课程专题列表
static async getSubjects(): Promise<ApiResponse<CourseSubject[]>> {
try {
console.log('🚀 获取课程专题列表')
// 调用后端API不需要token
const response = await ApiRequest.get<any>('/biz/course/subject/list')
console.log('🔍 专题API响应:', response)
// 处理后端响应格式
if (response.data && response.data.success && response.data.result) {
// 转换后端数据格式为前端格式专题接口返回的是value和label字段
const subjects: CourseSubject[] = response.data.result.map((item: any, index: number) => ({
id: item.value || '0', // 保持原始的value值字符串格式
name: item.label || '',
slug: item.label?.toLowerCase().replace(/\s+/g, '-') || '',
description: '',
sortOrder: index
}))
return {
code: 200,
message: '获取成功',
data: subjects
}
} else {
console.warn('⚠️ 专题API返回格式异常:', response)
return {
code: 500,
message: response.data?.message || '获取专题失败',
data: []
}
}
} catch (error: any) {
console.error('❌ 获取课程专题失败:', error)
return {
code: 500,
message: error.message || '获取专题失败',
data: []
}
}
}
// 获取课程难度列表
static async getDifficulties(): Promise<ApiResponse<CourseDifficulty[]>> {
try {
console.log('🚀 获取课程难度列表')
// 调用后端API不需要token
const response = await ApiRequest.get<any>('/biz/course/difficulty/list')
console.log('🔍 难度API响应:', response)
// 处理后端响应格式
if (response.data && response.data.success && response.data.result) {
// 转换后端数据格式为前端格式难度接口返回的是value和label字段
const difficulties: CourseDifficulty[] = response.data.result.map((item: any, index: number) => ({
id: item.value || '0', // 保持原始的value值字符串格式
name: item.label || '',
slug: item.label?.toLowerCase().replace(/\s+/g, '-') || '',
description: '',
sortOrder: index
}))
return {
code: 200,
message: '获取成功',
data: difficulties
}
} else {
console.warn('⚠️ 难度API返回格式异常:', response)
return {
code: 500,
message: response.data?.message || '获取难度失败',
data: []
}
}
} catch (error: any) {
console.error('❌ 获取课程难度失败:', error)
return {
code: 500,
message: error.message || '获取难度失败',
data: []
}
}
}
// 获取分类下的课程

View File

@ -12,9 +12,6 @@ const checkNetworkStatus = (): boolean => {
return true // 默认认为网络可用
}
// 全局开关强制使用Mock数据当前需求全部使用Mock不访问后端
const FORCE_ENABLE_MOCK = true
// 消息提示函数 - 使用window.alert作为fallback实际项目中应该使用UI库的消息组件
const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
// 这里可以替换为你使用的UI库的消息组件
@ -30,21 +27,36 @@ const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'i
// 创建axios实例
const request: AxiosInstance = axios.create({
// 统一走 /api由 Vite 代理到后端避免CORS若启用Mock则只会走本地Mock
baseURL: '/api',
baseURL: import.meta.env.VITE_API_BASE_URL || '/jeecgboot',
timeout: 30000, // 增加到30秒
headers: {
'Content-Type': 'application/json',
},
})
// 不需要token的接口列表
const NO_TOKEN_URLS = [
'/biz/course/category/list',
'/biz/course/subject/list',
'/biz/course/difficulty/list',
'/biz/course/list',
'/biz/course/detail',
// 可以在这里添加其他不需要token的接口
]
// 请求拦截器
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 添加认证token
// 检查是否需要添加token
const needToken = !NO_TOKEN_URLS.some(url => config.url?.includes(url))
if (needToken) {
// 添加认证token直接传token不需要Bearer前缀
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
const token = userStore.token || localStorage.getItem('X-Access-Token') || ''
if (token) {
config.headers['X-Access-Token'] = token
}
}
// 添加请求时间戳
@ -180,242 +192,7 @@ request.interceptors.response.use(
}
)
// 导入Mock数据
import { mockCourses, getMockCourseSections } from './mock/courses'
// getMockCourseDetail 暂时注释,后续需要时再启用
// Mock数据处理
const handleMockRequest = async <T = any>(url: string, method: string, data?: any): Promise<ApiResponse<T>> => {
console.log('🚀 Mock Request:', { url, method, data })
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 登录Mock
if (url === '/users/login' && method === 'POST') {
const { email, phone, password } = data || {}
const loginField = phone || email
// 模拟登录验证
if (loginField && password) {
return {
code: 200,
message: '登录成功',
data: {
user: {
id: 1,
email: email || `${phone}@example.com`,
phone: phone || '123456789',
username: phone || email?.split('@')[0] || 'user',
nickname: '测试用户',
avatar: 'https://via.placeholder.com/100',
role: 'student',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
},
token: 'mock_jwt_token_' + Date.now(),
refreshToken: 'mock_refresh_token_' + Date.now(),
expiresIn: 3600
}
} as ApiResponse<T>
} else {
return {
code: 400,
message: '手机号/邮箱或密码不能为空',
data: null
} as ApiResponse<T>
}
}
// 注册Mock
if (url === '/auth/register' && method === 'POST') {
const { email, password, verificationCode } = data || {}
if (!email || !password) {
return {
code: 400,
message: '邮箱和密码不能为空',
data: null
} as ApiResponse<T>
}
if (!verificationCode) {
return {
code: 400,
message: '验证码不能为空',
data: null
} as ApiResponse<T>
}
return {
code: 200,
message: '注册成功',
data: {
id: 2,
email: email,
username: email.split('@')[0],
nickname: '新用户',
avatar: '',
role: 'student',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
} as ApiResponse<T>
}
// 发送验证码Mock
if (url === '/auth/send-verification' && method === 'POST') {
return {
code: 200,
message: '验证码已发送',
data: null
} as ApiResponse<T>
}
// 获取当前用户信息Mock
if (url === '/auth/me' && method === 'GET') {
return {
code: 200,
message: '获取成功',
data: {
id: 1,
email: 'test@example.com',
username: 'test',
nickname: '测试用户',
avatar: '',
role: 'student',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
}
} as ApiResponse<T>
}
// 用户登出Mock
if (url === '/auth/logout' && method === 'POST') {
return {
code: 200,
message: '登出成功',
data: null
} as ApiResponse<T>
}
// 课程详情Mock
if (url === '/lesson/detail' && method === 'GET') {
// 对于GET请求参数直接在data中data就是params对象
const id = data?.id
console.log('课程详情Mock - 获取到的ID:', id, '原始data:', data)
if (!id) {
return {
code: 400,
message: '课程ID必填',
data: null
} as ApiResponse<T>
}
// 根据课程ID提供不同的模拟数据
const courseData = {
1: {
name: 'DeepSeek大语言模型实战应用',
cover: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=300&fit=crop',
price: '299.00',
school: 'DeepSeek技术学院',
description: '本课程深度聚焦DeepSeek大语言模型的实际应用让每一位学员了解并学习使用DeepSeek结合办公自动化职业岗位标准以实际工作任务为引导强调课程内容的易用性和岗位要求的匹配性。',
position: 'AI技术专家 / 高级讲师'
},
2: {
name: 'Python编程基础与实战',
cover: 'https://images.unsplash.com/photo-1526379095098-d400fd0bf935?w=400&h=300&fit=crop',
price: '199.00',
school: '编程技术学院',
description: '从零开始学习Python编程涵盖基础语法、数据结构、面向对象编程等核心概念通过实际项目练习掌握Python开发技能。',
position: 'Python开发专家 / 资深讲师'
},
3: {
name: 'Web前端开发全栈课程',
cover: 'https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=400&h=300&fit=crop',
price: '399.00',
school: '前端技术学院',
description: '全面学习现代Web前端开发技术包括HTML5、CSS3、JavaScript、Vue.js、React等主流框架培养全栈开发能力。',
position: '前端架构师 / 技术总监'
}
}
const currentCourse = courseData[id as keyof typeof courseData] || courseData[1]
// 模拟课程详情数据
return {
code: 0,
message: '查询课程详情成功',
data: {
id: parseInt(id),
name: currentCourse.name,
cover: currentCourse.cover,
categoryId: 1,
price: currentCourse.price,
school: currentCourse.school,
description: currentCourse.description,
teacherId: 1,
outline: '<div><h4>课程大纲:</h4><ul><li><strong>第一章:基础入门</strong><br/>- 环境搭建与配置<br/>- 基本概念理解<br/>- 实践操作演示</li><li><strong>第二章:核心技能</strong><br/>- 核心功能详解<br/>- 实际应用场景<br/>- 案例分析讲解</li><li><strong>第三章:高级应用</strong><br/>- 进阶技巧掌握<br/>- 项目实战演练<br/>- 问题解决方案</li></ul></div>',
prerequisite: '具备基本的计算机操作能力',
target: '掌握核心技能,能够在实际工作中熟练应用',
arrangement: '理论与实践相结合,循序渐进的学习方式',
startTime: '2025-01-26 10:13:17',
endTime: '2025-03-26 10:13:17',
revision: 1,
position: currentCourse.position,
createdAt: 1737944724,
updatedAt: 1737944724,
updatedTime: null
}
} as ApiResponse<T>
}
// 课程列表Mock
if (url === '/lesson/list' && method === 'GET') {
return {
code: 0,
message: '查询课程列表成功',
data: {
list: mockCourses,
total: mockCourses.length
}
} as ApiResponse<T>
}
// 课程章节列表Mock
if (url === '/lesson/section/list' && method === 'GET') {
const lessonId = data?.lesson_id
console.log('课程章节Mock - 获取到的lesson_id:', lessonId, '原始data:', data)
if (!lessonId) {
return {
code: 400,
message: '课程ID必填',
data: null
} as ApiResponse<T>
}
const mockSections = getMockCourseSections(parseInt(lessonId))
return {
code: 200,
message: '获取成功',
data: {
list: mockSections,
total: mockSections.length
},
timestamp: new Date().toISOString()
} as ApiResponse<T>
}
// 默认404响应
return {
code: 404,
message: '接口不存在',
data: null
} as ApiResponse<T>
}
// 重试机制
const retryRequest = async <T = any>(
@ -459,19 +236,7 @@ export class ApiRequest {
params?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
const enableMock = FORCE_ENABLE_MOCK || ((import.meta as any).env?.VITE_ENABLE_MOCK === 'true')
// 优先若显式启用Mock直接使用Mock
if (enableMock) {
return handleMockRequest<T>(url, 'GET', params)
}
try {
return await retryRequest(() => request.get(url, { params, ...config }))
} catch (error) {
console.warn('API请求失败降级到Mock数据:', error)
// 后备真实API失败时仍回落到Mock保障页面可用
return handleMockRequest<T>(url, 'GET', params)
}
}
// POST 请求
@ -480,17 +245,7 @@ export class ApiRequest {
data?: any,
config?: AxiosRequestConfig
): Promise<ApiResponse<T>> {
const enableMock = FORCE_ENABLE_MOCK || ((import.meta as any).env?.VITE_ENABLE_MOCK === 'true')
if (enableMock) {
return handleMockRequest<T>(url, 'POST', data)
}
try {
return await retryRequest(() => request.post(url, data, config))
} catch (error) {
console.warn('API请求失败降级到Mock数据:', error)
return handleMockRequest<T>(url, 'POST', data)
}
}
// PUT 请求

View File

@ -50,6 +50,7 @@ export interface UserProfile {
export interface LoginRequest {
email?: string
phone?: string
username?: string // 新增username字段用于适配后端API
password: string
captcha?: string
}
@ -81,7 +82,7 @@ export interface RegisterRequest {
// 课程相关类型
export interface Course {
id: number
id: string // 改为string类型保持后端ID的原始格式
title: string
subtitle?: string
description: string
@ -175,6 +176,132 @@ export interface CourseCategory {
children?: CourseCategory[]
}
// 后端分类数据格式
export interface BackendCourseCategory {
id: string
name: string
sortOrder: number
createBy: string
createTime: string
updateBy: string
updateTime: string
}
// 后端分类列表响应格式
export interface BackendCourseCategoryListResponse {
success: boolean
message: string
code: number
result: BackendCourseCategory[]
timestamp: number
}
// 后端专题数据格式(字典值格式)
export interface BackendCourseSubject {
value: string
label: string
}
// 前端专题数据格式
export interface CourseSubject {
id: string // 改为string类型保持后端value字段的原始格式
name: string
slug: string
description?: string
sortOrder: number
}
// 后端专题列表响应格式
export interface BackendCourseSubjectListResponse {
success: boolean
message: string
code: number
result: BackendCourseSubject[]
timestamp: number
}
// 后端难度数据格式(字典值格式)
export interface BackendCourseDifficulty {
value: string
label: string
}
// 前端难度数据格式
export interface CourseDifficulty {
id: string // 改为string类型保持后端value字段的原始格式
name: string
slug: string
description?: string
sortOrder: number
}
// 后端难度列表响应格式
export interface BackendCourseDifficultyListResponse {
success: boolean
message: string
code: number
result: BackendCourseDifficulty[]
timestamp: number
}
// 课程列表查询参数
export interface CourseListQueryParams {
categoryId?: string // 分类ID
difficulty?: string // 难度值
subject?: string // 专题值
}
// 后端课程数据格式
export interface BackendCourseItem {
id: string
name: string
cover: string
video: string
school: string
description: string
type: number
target: string
difficulty: number
subject: string
outline: string
prerequisite: string
reference: string
arrangement: string
startTime: string
endTime: string
enrollCount: number
maxEnroll: number
status: number
question: string
createBy: string
createTime: string
updateBy: string
updateTime: string
}
// 后端课程列表响应格式
export interface BackendCourseListResponse {
success: boolean
message: string
code: number
result: BackendCourseItem[]
timestamp: number
}
// 课程详情查询参数
export interface CourseDetailQueryParams {
id: string
}
// 后端课程详情响应格式
export interface BackendCourseDetailResponse {
success: boolean
message: string
code: number
result: BackendCourseItem
timestamp: number
}
export interface Instructor {
id: number
name: string

View File

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

View File

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

View File

@ -463,7 +463,7 @@ import RegisterModal from '@/components/auth/RegisterModal.vue'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const courseId = ref(Number(route.params.id))
const courseId = ref(route.params.id as string)
const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
// enrollCourse 使
@ -715,7 +715,7 @@ const displayComments = ref([
const loadCourseDetail = async () => {
console.log('开始加载课程详情课程ID:', courseId.value)
if (!courseId.value || isNaN(courseId.value)) {
if (!courseId.value || courseId.value.trim() === '') {
error.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
@ -768,7 +768,7 @@ const loadCourseDetail = async () => {
//
const loadCourseSections = async () => {
if (!courseId.value || isNaN(courseId.value)) {
if (!courseId.value || courseId.value.trim() === '') {
sectionsError.value = '课程ID无效'
console.error('课程ID无效:', courseId.value)
return
@ -1131,7 +1131,7 @@ onMounted(() => {
console.log('课程详情页加载完成课程ID:', courseId.value)
initializeMockState() //
loadCourseDetail()
loadCourseSections()
// loadCourseSections() //
})
</script>

View File

@ -17,70 +17,38 @@
<div class="filter-group">
<span class="filter-label">类型</span>
<div class="filter-tags">
<span class="filter-tag" :class="{ active: selectedSubject === '全部' }"
@click="selectSubject('全部')">全部</span>
<span class="filter-tag" :class="{ active: selectedSubject === '必修课' }"
@click="selectSubject('必修课')">必修课</span>
<span class="filter-tag" :class="{ active: selectedSubject === '高分课' }"
@click="selectSubject('高分课')">高分课</span>
<span class="filter-tag" :class="{ active: selectedSubject === '名师课堂' }"
@click="selectSubject('名师课堂')">名师课堂</span>
<span class="filter-tag" :class="{ active: selectedSubject === '训练营' }"
@click="selectSubject('训练营')">训练营</span>
<span class="filter-tag" :class="{ active: selectedSubject === '无考试' }"
@click="selectSubject('无考试')">无考试</span>
<span class="filter-tag" :class="{ active: selectedSubject === '专题讲座' }"
@click="selectSubject('专题讲座')">专题讲座</span>
<span class="filter-tag" :class="{ active: selectedMajor === '全部' }"
@click="selectMajor('全部')">全部</span>
<span
v-for="category in categories"
:key="category.id"
class="filter-tag"
:class="{ active: selectedMajor === category.name }"
@click="selectMajor(category.name)"
>
{{ category.name }}
</span>
<!-- 加载状态 -->
<span v-if="categoriesLoading" class="filter-tag loading">加载中...</span>
</div>
</div>
<!-- 专题分类第一行 -->
<!-- 专题分类 -->
<div class="filter-group">
<span class="filter-label">专题</span>
<div class="filter-tags">
<span class="filter-tag" :class="{ active: selectedMajor === '全部' }" @click="selectMajor('全部')">全部</span>
<span class="filter-tag" :class="{ active: selectedMajor === '学科教研' }"
@click="selectMajor('学科教研')">学科教研</span>
<span class="filter-tag" :class="{ active: selectedMajor === '班级管理' }"
@click="selectMajor('班级管理')">班级管理</span>
<span class="filter-tag" :class="{ active: selectedMajor === '通识技能' }"
@click="selectMajor('通识技能')">通识技能</span>
<span class="filter-tag" :class="{ active: selectedMajor === '信息素养' }"
@click="selectMajor('信息素养')">信息素养</span>
<span class="filter-tag" :class="{ active: selectedMajor === '师风师德' }"
@click="selectMajor('师风师德')">师风师德</span>
<span class="filter-tag" :class="{ active: selectedMajor === '专题教育' }"
@click="selectMajor('专题教育')">专题教育</span>
<span class="filter-tag" :class="{ active: selectedMajor === '综合实践' }"
@click="selectMajor('综合实践')">综合实践</span>
<span class="filter-tag" :class="{ active: selectedMajor === '个人成长' }"
@click="selectMajor('个人成长')">个人成长</span>
<span class="filter-tag" :class="{ active: selectedMajor === '教学案例' }"
@click="selectMajor('教学案例')">教学案例</span>
<span class="filter-tag" :class="{ active: selectedMajor === '教育技术' }"
@click="selectMajor('教育技术')">教育技术</span>
<span class="filter-tag" :class="{ active: selectedMajor === '心理健康' }"
@click="selectMajor('心理健康')">心理健康</span>
<span class="filter-tag" :class="{ active: selectedMajor === '家校沟通' }"
@click="selectMajor('家校沟通')">家校沟通</span>
<span class="filter-tag" :class="{ active: selectedMajor === '课程设计' }"
@click="selectMajor('课程设计')">课程设计</span>
<span class="filter-tag" :class="{ active: selectedMajor === '教育政策' }"
@click="selectMajor('教育政策')">教育政策</span>
<span class="filter-tag" :class="{ active: selectedMajor === '教学评估' }"
@click="selectMajor('教学评估')">教学评估</span>
<span class="filter-tag" :class="{ active: selectedMajor === '创新教育' }"
@click="selectMajor('创新教育')">创新教育</span>
<span class="filter-tag" :class="{ active: selectedMajor === 'STEAM教育' }"
@click="selectMajor('STEAM教育')">STEAM教育</span>
<span class="filter-tag" :class="{ active: selectedMajor === '教育心理学' }"
@click="selectMajor('教育心理学')">教育心理学</span>
<span class="filter-tag" :class="{ active: selectedMajor === '差异化教学' }"
@click="selectMajor('差异化教学')">差异化教学</span>
<span class="filter-tag" :class="{ active: selectedMajor === '教育领导力' }"
@click="selectMajor('教育领导力')">教育领导力</span>
<span class="filter-tag" :class="{ active: selectedMajor === '在线教学' }"
@click="selectMajor('在线教学')">在线教学</span>
<span class="filter-tag" :class="{ active: selectedSubject === '全部' }" @click="selectSubject('全部')">全部</span>
<span
v-for="subject in subjects"
:key="subject.id"
class="filter-tag"
:class="{ active: selectedSubject === subject.name }"
@click="selectSubject(subject.name)"
>
{{ subject.name }}
</span>
<!-- 加载状态 -->
<span v-if="subjectsLoading" class="filter-tag loading">加载中...</span>
</div>
</div>
@ -91,14 +59,17 @@
<div class="filter-tags">
<span class="filter-tag" :class="{ active: selectedDifficulty === '全部' }"
@click="selectDifficulty('全部')">全部</span>
<span class="filter-tag" :class="{ active: selectedDifficulty === '零基础' }"
@click="selectDifficulty('零基础')">零基础</span>
<span class="filter-tag" :class="{ active: selectedDifficulty === '初级' }"
@click="selectDifficulty('初级')">初级</span>
<span class="filter-tag" :class="{ active: selectedDifficulty === '中级' }"
@click="selectDifficulty('中级')">中级</span>
<span class="filter-tag" :class="{ active: selectedDifficulty === '高级' }"
@click="selectDifficulty('高级')">高级</span>
<span
v-for="difficulty in difficulties"
:key="difficulty.id"
class="filter-tag"
:class="{ active: selectedDifficulty === difficulty.name }"
@click="selectDifficulty(difficulty.name)"
>
{{ difficulty.name }}
</span>
<!-- 加载状态 -->
<span v-if="difficultiesLoading" class="filter-tag loading">加载中...</span>
</div>
</div>
</div>
@ -118,9 +89,9 @@
<!-- 排序标签 -->
<div class="sort-tabs">
<span class="sort-tab" :class="{ active: selectedSort === '最新' }" @click="selectSort('最新')">最新</span>
<span class="sort-tab" :class="{ active: selectedSort === '最热' }" @click="selectSort('最热')">最热</span>
<span class="sort-tab" :class="{ active: selectedSort === '推荐' }" @click="selectSort('推荐')">推荐</span>
<span class="sort-tab">最新</span>
<span class="sort-tab">最热</span>
<span class="sort-tab active">推荐</span>
</div>
<!-- 加载状态 -->
@ -203,8 +174,9 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
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 { CourseApi } from '@/api'
const router = useRouter()
@ -213,6 +185,18 @@ const courses = ref<Course[]>([])
const loading = ref(false)
const total = ref(0)
//
const categories = ref<CourseCategory[]>([])
const categoriesLoading = ref(false)
//
const subjects = ref<CourseSubject[]>([])
const subjectsLoading = ref(false)
//
const difficulties = ref<CourseDifficulty[]>([])
const difficultiesLoading = ref(false)
//
const selectedSubject = ref('全部')
const selectedMajor = ref('全部')
@ -224,9 +208,6 @@ const itemsPerPage = 20
const totalItems = computed(() => total.value)
const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage))
//
const selectedSort = ref('推荐')
// 广
const showAdvertisement = ref(true)
@ -276,84 +257,59 @@ const visiblePages = computed(() => {
return pages
})
//
const selectSort = (sort: string) => {
selectedSort.value = sort
currentPage.value = 1 //
loadCourses()
}
// 使
// 使API
const loadCourses = async () => {
try {
loading.value = true
console.log('🚀 加载课程数据...')
//
await new Promise(resolve => setTimeout(resolve, 500))
//
const queryParams: any = {}
//
let filteredCourses = [...mockCourses]
//
if (selectedSubject.value !== '全部') {
filteredCourses = filteredCourses.filter(course => {
switch (selectedSubject.value) {
case '计算机':
return course.category.name === '编程开发' || course.category.name === '前端开发' || course.category.name === '后端开发'
case '教育学':
return course.category.name === '教育培训'
default:
return true
}
})
}
//
switch (selectedSort.value) {
case '最新':
filteredCourses.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
break
case '最热':
filteredCourses.sort((a, b) => b.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
}
//
// categoryId{id, name}id
if (selectedMajor.value !== '全部') {
filteredCourses = filteredCourses.filter(course =>
course.title.includes(selectedMajor.value) ||
course.description.includes(selectedMajor.value) ||
course.tags.some(tag => tag.includes(selectedMajor.value))
)
const selectedCategory = categories.value.find(cat => cat.name === selectedMajor.value)
if (selectedCategory) {
queryParams.categoryId = selectedCategory.id.toString()
console.log('🏷️ 选择的分类:', selectedCategory.name, 'ID:', selectedCategory.id)
}
}
//
// difficulty{value, label}value
if (selectedDifficulty.value !== '全部') {
const difficultyMap: { [key: string]: string } = {
'初级': 'beginner',
'中级': 'intermediate',
'高级': 'advanced'
}
const targetLevel = difficultyMap[selectedDifficulty.value]
if (targetLevel) {
filteredCourses = filteredCourses.filter(course => course.level === targetLevel)
const selectedDiff = difficulties.value.find(diff => diff.name === selectedDifficulty.value)
if (selectedDiff) {
queryParams.difficulty = selectedDiff.id // 使toString()
console.log('📊 选择的难度:', selectedDiff.name, 'Value:', selectedDiff.id)
}
}
//
total.value = filteredCourses.length
const startIndex = (currentPage.value - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
courses.value = filteredCourses.slice(startIndex, endIndex)
// subject{value, label}value
if (selectedSubject.value !== '全部') {
const selectedSubj = subjects.value.find(subj => subj.name === selectedSubject.value)
if (selectedSubj) {
queryParams.subject = selectedSubj.id // 使toString()
console.log('🎯 选择的专题:', selectedSubj.name, 'Value:', selectedSubj.id)
}
}
console.log('课程加载成功:', courses.value.length, '条课程,总计:', total.value)
console.log('🔍 查询参数:', queryParams)
// API
const response = await CourseApi.getCourses(queryParams)
console.log('✅ 课程API响应:', response)
if (response.code === 200 && response.data) {
courses.value = response.data
total.value = response.data.length
console.log('✅ 课程数据加载成功:', courses.value.length, '条课程')
} else {
console.warn('⚠️ 课程数据加载失败:', response.message)
courses.value = []
total.value = 0
}
} catch (error) {
console.error('加载课程失败:', error)
console.error('加载课程失败:', error)
courses.value = []
total.value = 0
} finally {
@ -442,9 +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(() => {
loadCourses()
loadCategories()
loadSubjects()
loadDifficulties()
})
</script>
@ -617,6 +642,13 @@ onMounted(() => {
font-weight: 500;
}
.filter-tag.loading {
background: #f0f0f0;
color: #999;
cursor: not-allowed;
opacity: 0.7;
}
.sort-tabs {
display: flex;
gap: 20px;

View File

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