Compare commits
2 Commits
59c4d9ebc5
...
e645a190dd
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e645a190dd | ||
![]() |
21845cb21a |
@ -5,6 +5,7 @@ export * from './request'
|
|||||||
// 导出所有API模块
|
// 导出所有API模块
|
||||||
export { default as AuthApi } from './modules/auth'
|
export { default as AuthApi } from './modules/auth'
|
||||||
export { default as CourseApi } from './modules/course'
|
export { default as CourseApi } from './modules/course'
|
||||||
|
export { default as ChapterApi } from './modules/chapter'
|
||||||
export { default as CommentApi } from './modules/comment'
|
export { default as CommentApi } from './modules/comment'
|
||||||
export { default as FavoriteApi } from './modules/favorite'
|
export { default as FavoriteApi } from './modules/favorite'
|
||||||
export { default as OrderApi } from './modules/order'
|
export { default as OrderApi } from './modules/order'
|
||||||
|
314
src/api/modules/chapter.ts
Normal file
314
src/api/modules/chapter.ts
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
// 课程章节相关API接口
|
||||||
|
import { ApiRequest } from '../request'
|
||||||
|
import type {
|
||||||
|
ApiResponse,
|
||||||
|
CourseSection,
|
||||||
|
CourseSectionListResponse,
|
||||||
|
BackendCourseSection,
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
|
// 章节查询参数类型
|
||||||
|
export interface ChapterQueryParams {
|
||||||
|
courseId: string
|
||||||
|
keyword?: string
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
type?: number | null // 章节类型:0=视频、1=资料、2=考试、3=作业,null=全部
|
||||||
|
parentId?: string // 父章节ID,用于查询子章节
|
||||||
|
level?: number // 章节层级:0=一级章节、1=二级章节
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程章节API模块
|
||||||
|
*/
|
||||||
|
export class ChapterApi {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取课程章节列表
|
||||||
|
* @param params 查询参数
|
||||||
|
* @returns 章节列表响应
|
||||||
|
*/
|
||||||
|
static async getChapters(params: ChapterQueryParams): Promise<ApiResponse<CourseSectionListResponse>> {
|
||||||
|
try {
|
||||||
|
console.log('🚀 调用课程章节列表API,参数:', params)
|
||||||
|
|
||||||
|
// 构建查询参数 - courseId作为Path参数,其他作为Query参数
|
||||||
|
const queryParams: any = {}
|
||||||
|
|
||||||
|
if (params.keyword) queryParams.keyword = params.keyword
|
||||||
|
if (params.type !== undefined) queryParams.type = params.type
|
||||||
|
if (params.parentId) queryParams.parentId = params.parentId
|
||||||
|
if (params.level !== undefined) queryParams.level = params.level
|
||||||
|
if (params.page) queryParams.page = params.page
|
||||||
|
if (params.pageSize) queryParams.pageSize = params.pageSize
|
||||||
|
|
||||||
|
console.log('🔍 Path参数 courseId (token):', params.courseId)
|
||||||
|
console.log('🔍 Query参数:', queryParams)
|
||||||
|
|
||||||
|
// 调用后端API - courseId作为Path参数,其他作为Query参数
|
||||||
|
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${params.courseId}/section`, queryParams)
|
||||||
|
console.log('🔍 章节列表API响应:', response)
|
||||||
|
|
||||||
|
// 处理后端响应格式
|
||||||
|
let rawData = null;
|
||||||
|
if (response.data && response.data.success && response.data.result) {
|
||||||
|
rawData = response.data.result;
|
||||||
|
console.log('✅ 响应数据来源: result字段');
|
||||||
|
} else if (response.data && response.data.list) {
|
||||||
|
rawData = response.data.list;
|
||||||
|
console.log('✅ 响应数据来源: list字段');
|
||||||
|
} else if (Array.isArray(response.data)) {
|
||||||
|
rawData = response.data;
|
||||||
|
console.log('✅ 响应数据来源: 直接数组');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawData && Array.isArray(rawData)) {
|
||||||
|
console.log('✅ 原始章节数据:', rawData)
|
||||||
|
console.log('✅ 章节数据数量:', rawData.length)
|
||||||
|
|
||||||
|
// 适配数据格式 - 直接使用原始数据,因为字段名已经匹配
|
||||||
|
const adaptedSections: CourseSection[] = rawData.map((section: any) => ({
|
||||||
|
id: section.id,
|
||||||
|
lessonId: section.lessonId, // 直接使用原始字段
|
||||||
|
outline: section.outline || '', // 直接使用原始字段
|
||||||
|
name: section.name,
|
||||||
|
type: section.type, // 直接使用原始字段
|
||||||
|
parentId: section.parentId || '', // 直接使用原始字段
|
||||||
|
sort: section.sort, // 直接使用原始字段
|
||||||
|
level: section.level,
|
||||||
|
revision: section.revision || 1, // 直接使用原始字段
|
||||||
|
createdAt: section.createdAt, // 直接使用原始字段
|
||||||
|
updatedAt: section.updatedAt, // 直接使用原始字段
|
||||||
|
deletedAt: section.deletedAt,
|
||||||
|
completed: section.completed || false,
|
||||||
|
duration: section.duration
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log('✅ 适配后的章节数据:', adaptedSections)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
list: adaptedSections,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
traceId: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ API返回的数据结构不正确:', response.data)
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: '数据格式错误',
|
||||||
|
data: {
|
||||||
|
list: [],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
traceId: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 章节API调用失败:', error)
|
||||||
|
console.error('❌ 错误详情:', {
|
||||||
|
message: (error as Error).message,
|
||||||
|
stack: (error as Error).stack,
|
||||||
|
response: (error as any).response?.data,
|
||||||
|
status: (error as any).response?.status,
|
||||||
|
statusText: (error as any).response?.statusText
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重新抛出错误,不使用模拟数据
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索课程章节
|
||||||
|
* @param params 搜索参数
|
||||||
|
* @returns 搜索结果
|
||||||
|
*/
|
||||||
|
static async searchChapters(params: ChapterQueryParams): Promise<ApiResponse<CourseSectionListResponse>> {
|
||||||
|
try {
|
||||||
|
console.log('🔍 搜索课程章节,参数:', params)
|
||||||
|
|
||||||
|
// 构建搜索参数 - courseId作为Path参数,keyword等作为Query参数
|
||||||
|
const searchParams: any = {}
|
||||||
|
|
||||||
|
if (params.keyword) searchParams.keyword = params.keyword
|
||||||
|
if (params.type !== undefined) searchParams.type = params.type
|
||||||
|
if (params.parentId) searchParams.parentId = params.parentId
|
||||||
|
if (params.level !== undefined) searchParams.level = params.level
|
||||||
|
if (params.page) searchParams.page = params.page
|
||||||
|
if (params.pageSize) searchParams.pageSize = params.pageSize
|
||||||
|
|
||||||
|
console.log('🔍 Path参数 courseId (token):', params.courseId)
|
||||||
|
console.log('🔍 Query参数 (包含keyword):', searchParams)
|
||||||
|
|
||||||
|
// 调用后端API - courseId作为Path参数,keyword等作为Query参数
|
||||||
|
const response = await ApiRequest.get<any>(`/aiol/aiolCourse/${params.courseId}/section`, searchParams)
|
||||||
|
console.log('🔍 章节搜索API响应:', response)
|
||||||
|
|
||||||
|
// 处理后端响应格式
|
||||||
|
if (response.data && response.data.success && response.data.result) {
|
||||||
|
console.log('✅ 搜索响应状态码:', response.data.code)
|
||||||
|
console.log('✅ 搜索响应消息:', response.data.message)
|
||||||
|
console.log('✅ 搜索结果数据:', response.data.result)
|
||||||
|
console.log('✅ 搜索结果数量:', response.data.result.length || 0)
|
||||||
|
|
||||||
|
// 适配数据格式 - 使用BackendCourseSection类型
|
||||||
|
const adaptedSections: CourseSection[] = response.data.result.map((section: BackendCourseSection) => ({
|
||||||
|
id: section.id,
|
||||||
|
lessonId: section.courseId, // 使用BackendCourseSection字段
|
||||||
|
outline: '',
|
||||||
|
name: section.name,
|
||||||
|
type: section.type,
|
||||||
|
parentId: section.parentId || '', // 使用BackendCourseSection字段
|
||||||
|
sort: section.sortOrder, // 使用BackendCourseSection字段
|
||||||
|
level: section.level,
|
||||||
|
revision: 1,
|
||||||
|
createdAt: section.createTime ? new Date(section.createTime).getTime() : null, // 使用BackendCourseSection字段
|
||||||
|
updatedAt: section.updateTime ? new Date(section.updateTime).getTime() : null, // 使用BackendCourseSection字段
|
||||||
|
deletedAt: null,
|
||||||
|
completed: false,
|
||||||
|
duration: undefined
|
||||||
|
}))
|
||||||
|
|
||||||
|
console.log('✅ 适配后的搜索结果:', adaptedSections)
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: response.data.code,
|
||||||
|
message: response.data.message,
|
||||||
|
data: {
|
||||||
|
list: adaptedSections,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
traceId: response.data.timestamp?.toString() || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ 搜索API返回的数据结构不正确:', response.data)
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: '搜索数据格式错误',
|
||||||
|
data: {
|
||||||
|
list: [],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
traceId: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 章节搜索API调用失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新建课程章节
|
||||||
|
* @param sectionData 章节数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
static async createChapter(sectionData: any): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
console.log('🚀 调用新建章节API,数据:', sectionData)
|
||||||
|
|
||||||
|
// 包装数据为aiolCourseSectionDTO格式
|
||||||
|
const requestData = {
|
||||||
|
aiolCourseSectionDTO: sectionData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用后端API - 新建章节
|
||||||
|
const response = await ApiRequest.post<any>('/aiol/aiolCourseSection/add', requestData)
|
||||||
|
console.log('🔍 新建章节API响应:', response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 新建章节失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑课程章节
|
||||||
|
* @param sectionData 章节数据
|
||||||
|
* @returns 编辑结果
|
||||||
|
*/
|
||||||
|
static async editChapter(sectionData: any): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
console.log('🚀 调用编辑章节API,数据:', sectionData)
|
||||||
|
|
||||||
|
// 尝试不同的数据格式
|
||||||
|
const requestData = {
|
||||||
|
...sectionData,
|
||||||
|
// 确保所有必要字段都存在
|
||||||
|
id: sectionData.id,
|
||||||
|
name: sectionData.name,
|
||||||
|
courseId: sectionData.courseId,
|
||||||
|
type: sectionData.type || 0,
|
||||||
|
sortOrder: sectionData.sortOrder || 10,
|
||||||
|
parentId: sectionData.parentId || '0',
|
||||||
|
level: sectionData.level || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 发送给服务器的完整请求数据:', JSON.stringify(requestData, null, 2))
|
||||||
|
console.log('🔍 章节ID:', sectionData.id)
|
||||||
|
console.log('🔍 章节名称:', sectionData.name)
|
||||||
|
console.log('🔍 课程ID:', sectionData.courseId)
|
||||||
|
|
||||||
|
// 调用后端API - 编辑章节
|
||||||
|
// 使用原来的edit路径
|
||||||
|
const response = await ApiRequest.post<any>('/aiol/aiolCourseSection/edit', requestData)
|
||||||
|
console.log('🔍 编辑章节API响应:', response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 编辑章节失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除课程章节
|
||||||
|
* @param sectionId 章节ID
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
static async deleteChapter(sectionId: string): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
console.log('🚀 调用删除章节API,章节ID:', sectionId)
|
||||||
|
|
||||||
|
// 调用后端API - 删除章节
|
||||||
|
const response = await ApiRequest.delete<any>('/aiol/aiolCourseSection/delete', {
|
||||||
|
id: sectionId
|
||||||
|
})
|
||||||
|
console.log('🔍 删除章节API响应:', response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 删除章节失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除课程章节
|
||||||
|
* @param sectionIds 章节ID数组
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
static async deleteChaptersBatch(sectionIds: string[]): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
console.log('🚀 调用批量删除章节API,章节IDs:', sectionIds)
|
||||||
|
|
||||||
|
// 调用后端API - 批量删除章节
|
||||||
|
const response = await ApiRequest.delete<any>('/aiol/aiolCourseSection/deleteBatch', {
|
||||||
|
ids: sectionIds.join(',')
|
||||||
|
})
|
||||||
|
console.log('🔍 批量删除章节API响应:', response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 批量删除章节失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChapterApi
|
@ -333,6 +333,422 @@ export class ExamApi {
|
|||||||
console.log('✅ 批量添加题目答案成功:', responses)
|
console.log('✅ 批量添加题目答案成功:', responses)
|
||||||
return responses
|
return responses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 试卷管理相关接口 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试卷列表
|
||||||
|
*/
|
||||||
|
static async getExamPaperList(params: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
category?: string
|
||||||
|
status?: string
|
||||||
|
difficulty?: string
|
||||||
|
creator?: string
|
||||||
|
} = {}): Promise<ApiResponse<{
|
||||||
|
result: {
|
||||||
|
records: any[]
|
||||||
|
total: number
|
||||||
|
current: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
}>> {
|
||||||
|
console.log('🚀 获取试卷列表:', params)
|
||||||
|
const response = await ApiRequest.get<{
|
||||||
|
result: {
|
||||||
|
records: any[]
|
||||||
|
total: number
|
||||||
|
current: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
}>('/aiol/aiolPaper/list', { params })
|
||||||
|
console.log('✅ 获取试卷列表成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试卷详情
|
||||||
|
*/
|
||||||
|
static async getExamPaperDetail(id: string): Promise<ApiResponse<any>> {
|
||||||
|
console.log('🚀 获取试卷详情:', id)
|
||||||
|
const response = await ApiRequest.get<any>(`/aiol/aiolExam/paperDetail/${id}`)
|
||||||
|
console.log('✅ 获取试卷详情成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建试卷
|
||||||
|
*/
|
||||||
|
static async createExamPaper(data: {
|
||||||
|
name: string
|
||||||
|
category: string
|
||||||
|
description?: string
|
||||||
|
totalScore: number
|
||||||
|
difficulty: string
|
||||||
|
duration: number
|
||||||
|
questions: any[]
|
||||||
|
}): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 创建试卷:', data)
|
||||||
|
const response = await ApiRequest.post<string>('/aiol/aiolPaper/add', data)
|
||||||
|
console.log('✅ 创建试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新试卷
|
||||||
|
*/
|
||||||
|
static async updateExamPaper(id: string, data: {
|
||||||
|
name?: string
|
||||||
|
category?: string
|
||||||
|
description?: string
|
||||||
|
totalScore?: number
|
||||||
|
difficulty?: string
|
||||||
|
duration?: number
|
||||||
|
questions?: any[]
|
||||||
|
}): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 更新试卷:', { id, data })
|
||||||
|
const response = await ApiRequest.put<string>(`/aiol/aiolExam/paperUpdate/${id}`, data)
|
||||||
|
console.log('✅ 更新试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除试卷
|
||||||
|
*/
|
||||||
|
static async deleteExamPaper(id: string): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 删除试卷:', id)
|
||||||
|
const response = await ApiRequest.delete<string>(`/aiol/aiolExam/paperDelete/${id}`)
|
||||||
|
console.log('✅ 删除试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除试卷
|
||||||
|
*/
|
||||||
|
static async batchDeleteExamPapers(ids: string[]): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 批量删除试卷:', ids)
|
||||||
|
const response = await ApiRequest.post<string>('/aiol/aiolExam/paperBatchDelete', { ids })
|
||||||
|
console.log('✅ 批量删除试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布试卷
|
||||||
|
*/
|
||||||
|
static async publishExamPaper(id: string, data: {
|
||||||
|
startTime: string
|
||||||
|
endTime: string
|
||||||
|
classIds?: string[]
|
||||||
|
}): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 发布试卷:', { id, data })
|
||||||
|
const response = await ApiRequest.post<string>(`/aiol/aiolExam/paperPublish/${id}`, data)
|
||||||
|
console.log('✅ 发布试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消发布试卷
|
||||||
|
*/
|
||||||
|
static async unpublishExamPaper(id: string): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 取消发布试卷:', id)
|
||||||
|
const response = await ApiRequest.post<string>(`/aiol/aiolExam/paperUnpublish/${id}`)
|
||||||
|
console.log('✅ 取消发布试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束试卷
|
||||||
|
*/
|
||||||
|
static async endExamPaper(id: string): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 结束试卷:', id)
|
||||||
|
const response = await ApiRequest.post<string>(`/aiol/aiolExam/paperEnd/${id}`)
|
||||||
|
console.log('✅ 结束试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入试卷
|
||||||
|
*/
|
||||||
|
static async importExamPaper(file: File): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 导入试卷:', file.name)
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const response = await ApiRequest.post<string>('/aiol/aiolExam/paperImport', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('✅ 导入试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出试卷
|
||||||
|
*/
|
||||||
|
static async exportExamPaper(id: string): Promise<ApiResponse<Blob>> {
|
||||||
|
console.log('🚀 导出试卷:', id)
|
||||||
|
const response = await ApiRequest.get<Blob>(`/aiol/aiolExam/paperExport/${id}`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
console.log('✅ 导出试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量导出试卷
|
||||||
|
*/
|
||||||
|
static async batchExportExamPapers(ids: string[]): Promise<ApiResponse<Blob>> {
|
||||||
|
console.log('🚀 批量导出试卷:', ids)
|
||||||
|
const response = await ApiRequest.post<Blob>('/aiol/aiolExam/paperBatchExport', { ids }, {
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
console.log('✅ 批量导出试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取试卷分析数据
|
||||||
|
*/
|
||||||
|
static async getExamPaperAnalysis(id: string): Promise<ApiResponse<{
|
||||||
|
totalStudents: number
|
||||||
|
submittedCount: number
|
||||||
|
averageScore: number
|
||||||
|
passRate: number
|
||||||
|
scoreDistribution: Array<{ score: number; count: number }>
|
||||||
|
questionAnalysis: Array<{
|
||||||
|
questionId: string
|
||||||
|
correctRate: number
|
||||||
|
averageScore: number
|
||||||
|
}>
|
||||||
|
}>> {
|
||||||
|
console.log('🚀 获取试卷分析:', id)
|
||||||
|
const response = await ApiRequest.get<{
|
||||||
|
totalStudents: number
|
||||||
|
submittedCount: number
|
||||||
|
averageScore: number
|
||||||
|
passRate: number
|
||||||
|
scoreDistribution: Array<{ score: number; count: number }>
|
||||||
|
questionAnalysis: Array<{
|
||||||
|
questionId: string
|
||||||
|
correctRate: number
|
||||||
|
averageScore: number
|
||||||
|
}>
|
||||||
|
}>(`/aiol/aiolExam/paperAnalysis/${id}`)
|
||||||
|
console.log('✅ 获取试卷分析成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 阅卷中心相关接口 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取阅卷列表(考试列表)
|
||||||
|
*/
|
||||||
|
static async getMarkingList(params: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
status?: 'all' | 'not-started' | 'in-progress' | 'completed'
|
||||||
|
examType?: string
|
||||||
|
className?: string
|
||||||
|
keyword?: string
|
||||||
|
} = {}): Promise<ApiResponseWithResult<{
|
||||||
|
list: any[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}>> {
|
||||||
|
console.log('🚀 获取阅卷列表:', params)
|
||||||
|
const response = await ApiRequest.get<{
|
||||||
|
result: {
|
||||||
|
list: any[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
}>('/aiol/aiolExam/list', { params })
|
||||||
|
console.log('✅ 获取阅卷列表成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取考试统计信息
|
||||||
|
*/
|
||||||
|
static async getExamStats(examId: string): Promise<ApiResponse<{
|
||||||
|
totalStudents: number
|
||||||
|
submittedCount: number
|
||||||
|
gradedCount: number
|
||||||
|
averageScore: number
|
||||||
|
passRate: number
|
||||||
|
}>> {
|
||||||
|
console.log('🚀 获取考试统计:', { examId })
|
||||||
|
const response = await ApiRequest.get<{
|
||||||
|
totalStudents: number
|
||||||
|
submittedCount: number
|
||||||
|
gradedCount: number
|
||||||
|
averageScore: number
|
||||||
|
passRate: number
|
||||||
|
}>(`/aiol/aiolExam/stats/${examId}`)
|
||||||
|
console.log('✅ 获取考试统计成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取学生答题列表
|
||||||
|
*/
|
||||||
|
static async getStudentAnswers(examId: string, params: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
status?: 'all' | 'submitted' | 'not-submitted'
|
||||||
|
className?: string
|
||||||
|
keyword?: string
|
||||||
|
} = {}): Promise<ApiResponseWithResult<{
|
||||||
|
list: any[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}>> {
|
||||||
|
console.log('🚀 获取学生答题列表:', { examId, params })
|
||||||
|
const response = await ApiRequest.get<{
|
||||||
|
result: {
|
||||||
|
list: any[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
}>(`/aiol/aiolExam/students/${examId}`, { params })
|
||||||
|
console.log('✅ 获取学生答题列表成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取学生答题详情
|
||||||
|
*/
|
||||||
|
static async getStudentAnswerDetail(examId: string, studentId: string): Promise<ApiResponse<{
|
||||||
|
studentInfo: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
studentId: string
|
||||||
|
className: string
|
||||||
|
avatar?: string
|
||||||
|
examDuration: string
|
||||||
|
submitTime: string
|
||||||
|
}
|
||||||
|
questions: Array<{
|
||||||
|
id: string
|
||||||
|
number: number
|
||||||
|
type: string
|
||||||
|
content: string
|
||||||
|
score: number
|
||||||
|
options?: Array<{ key: string; text: string }>
|
||||||
|
correctAnswer: string[]
|
||||||
|
correctAnswerText?: string
|
||||||
|
explanation?: string
|
||||||
|
studentAnswer?: string[]
|
||||||
|
studentTextAnswer?: string
|
||||||
|
isCorrect?: boolean | null
|
||||||
|
studentScore?: number
|
||||||
|
}>
|
||||||
|
gradingComments?: string
|
||||||
|
}>> {
|
||||||
|
console.log('🚀 获取学生答题详情:', { examId, studentId })
|
||||||
|
const response = await ApiRequest.get<{
|
||||||
|
studentInfo: any
|
||||||
|
questions: any[]
|
||||||
|
gradingComments?: string
|
||||||
|
}>(`/aiol/aiolExam/student/${examId}/${studentId}/answer`)
|
||||||
|
console.log('✅ 获取学生答题详情成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批阅试卷
|
||||||
|
*/
|
||||||
|
static async gradeExam(examId: string, studentId: string, data: {
|
||||||
|
questions: Array<{
|
||||||
|
id: string
|
||||||
|
isCorrect: boolean | null
|
||||||
|
studentScore: number
|
||||||
|
}>
|
||||||
|
gradingComments?: string
|
||||||
|
totalScore: number
|
||||||
|
}): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 批阅试卷:', { examId, studentId, data })
|
||||||
|
const response = await ApiRequest.post<string>(`/aiol/aiolExam/grade/${examId}/${studentId}`, data)
|
||||||
|
console.log('✅ 批阅试卷成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取班级列表(用于筛选)
|
||||||
|
*/
|
||||||
|
static async getClassList(): Promise<ApiResponse<Array<{ id: string; name: string }>>> {
|
||||||
|
console.log('🚀 获取班级列表')
|
||||||
|
try {
|
||||||
|
// 尝试多个可能的接口路径
|
||||||
|
const possiblePaths = [
|
||||||
|
'/aiol/aiolClass/list',
|
||||||
|
'/aiol/aiolClass/queryList',
|
||||||
|
'/aiol/aiolClass/page',
|
||||||
|
'/aiol/aiolClass/queryPage',
|
||||||
|
'/aiol/aiolExam/classes',
|
||||||
|
'/aiol/aiolExam/classList'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const path of possiblePaths) {
|
||||||
|
try {
|
||||||
|
console.log(`尝试接口路径: ${path}`)
|
||||||
|
const response = await ApiRequest.get<Array<{ id: string; name: string }>>(path)
|
||||||
|
console.log(`✅ 获取班级列表成功 (${path}):`, response)
|
||||||
|
return response
|
||||||
|
} catch (pathError) {
|
||||||
|
console.warn(`接口 ${path} 不存在:`, pathError)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果所有接口都不存在,返回空数组
|
||||||
|
console.warn('所有班级列表接口都不存在,返回空数组')
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'success',
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取班级列表失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出考试结果
|
||||||
|
*/
|
||||||
|
static async exportExamResults(examId: string, params: {
|
||||||
|
className?: string
|
||||||
|
status?: string
|
||||||
|
} = {}): Promise<Blob> {
|
||||||
|
console.log('🚀 导出考试结果:', { examId, params })
|
||||||
|
const response = await ApiRequest.get<Blob>(`/aiol/aiolExam/export/${examId}`, {
|
||||||
|
params,
|
||||||
|
responseType: 'blob'
|
||||||
|
})
|
||||||
|
console.log('✅ 导出考试结果成功:', response)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布补考
|
||||||
|
*/
|
||||||
|
static async publishRetakeExam(examId: string, data: {
|
||||||
|
studentIds: string[]
|
||||||
|
retakeTime: string
|
||||||
|
retakeDuration: number
|
||||||
|
}): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 发布补考:', { examId, data })
|
||||||
|
const response = await ApiRequest.post<string>(`/aiol/aiolExam/retake/${examId}`, data)
|
||||||
|
console.log('✅ 发布补考成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExamApi
|
export default ExamApi
|
@ -462,6 +462,17 @@ export interface BackendCourseSectionListResponse {
|
|||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 章节查询参数类型
|
||||||
|
export interface ChapterQueryParams {
|
||||||
|
courseId: string
|
||||||
|
keyword?: string
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
type?: number | null // 章节类型:0=视频、1=资料、2=考试、3=作业,null=全部
|
||||||
|
parentId?: string // 父章节ID,用于查询子章节
|
||||||
|
level?: number // 章节层级:0=一级章节、1=二级章节
|
||||||
|
}
|
||||||
|
|
||||||
// 后端讲师数据结构
|
// 后端讲师数据结构
|
||||||
export interface BackendInstructor {
|
export interface BackendInstructor {
|
||||||
id: string
|
id: string
|
||||||
|
@ -289,6 +289,7 @@ import TrueFalseQuestion from '@/components/teacher/TrueFalseQuestion.vue';
|
|||||||
import FillBlankQuestion from '@/components/teacher/FillBlankQuestion.vue';
|
import FillBlankQuestion from '@/components/teacher/FillBlankQuestion.vue';
|
||||||
import ShortAnswerQuestion from '@/components/teacher/ShortAnswerQuestion.vue';
|
import ShortAnswerQuestion from '@/components/teacher/ShortAnswerQuestion.vue';
|
||||||
import CompositeQuestion from '@/components/teacher/CompositeQuestion.vue';
|
import CompositeQuestion from '@/components/teacher/CompositeQuestion.vue';
|
||||||
|
import { ExamApi } from '@/api/modules/exam';
|
||||||
|
|
||||||
// 创建独立的 dialog API
|
// 创建独立的 dialog API
|
||||||
const { dialog } = createDiscreteApi(['dialog'])
|
const { dialog } = createDiscreteApi(['dialog'])
|
||||||
@ -998,7 +999,7 @@ const getQuestionTypeFromString = (typeString: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 保存试卷
|
// 保存试卷
|
||||||
const saveExam = () => {
|
const saveExam = async () => {
|
||||||
// 验证数据
|
// 验证数据
|
||||||
if (!examForm.title.trim()) {
|
if (!examForm.title.trim()) {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
@ -1035,14 +1036,69 @@ const saveExam = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这里实现保存逻辑
|
try {
|
||||||
console.log('保存试卷数据:', examForm);
|
// 准备API数据
|
||||||
|
const apiData = {
|
||||||
|
name: examForm.title,
|
||||||
|
category: examForm.type === 1 ? '考试' : '练习',
|
||||||
|
description: examForm.description || '',
|
||||||
|
totalScore: examForm.totalScore,
|
||||||
|
difficulty: getDifficultyLevel(examForm.totalScore),
|
||||||
|
duration: examForm.duration,
|
||||||
|
questions: formatQuestionsForAPI(examForm.questions)
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🚀 准备保存试卷数据:', apiData);
|
||||||
|
|
||||||
|
// 调用API创建试卷
|
||||||
|
const response = await ExamApi.createExamPaper(apiData);
|
||||||
|
console.log('✅ 创建试卷成功:', response);
|
||||||
|
|
||||||
dialog.success({
|
dialog.success({
|
||||||
title: '保存成功',
|
title: '保存成功',
|
||||||
content: '试卷保存成功!',
|
content: '试卷保存成功!',
|
||||||
|
positiveText: '确定',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
// 保存成功后返回试卷列表页面
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建试卷失败:', error);
|
||||||
|
dialog.error({
|
||||||
|
title: '保存失败',
|
||||||
|
content: '试卷保存失败,请重试',
|
||||||
positiveText: '确定'
|
positiveText: '确定'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据总分计算难度等级
|
||||||
|
const getDifficultyLevel = (totalScore: number): string => {
|
||||||
|
if (totalScore <= 60) return 'easy';
|
||||||
|
if (totalScore <= 100) return 'medium';
|
||||||
|
return 'hard';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化题目数据为API需要的格式
|
||||||
|
const formatQuestionsForAPI = (questions: any[]): any[] => {
|
||||||
|
return questions.map((bigQuestion, index) => ({
|
||||||
|
id: bigQuestion.id,
|
||||||
|
title: bigQuestion.title,
|
||||||
|
description: bigQuestion.description,
|
||||||
|
sort: index + 1,
|
||||||
|
totalScore: bigQuestion.totalScore,
|
||||||
|
subQuestions: bigQuestion.subQuestions.map((subQuestion: any, subIndex: number) => ({
|
||||||
|
id: subQuestion.id,
|
||||||
|
title: subQuestion.title,
|
||||||
|
type: subQuestion.type,
|
||||||
|
options: subQuestion.options || [],
|
||||||
|
correctAnswer: subQuestion.correctAnswer,
|
||||||
|
score: subQuestion.score,
|
||||||
|
sort: subIndex + 1
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// 预览试卷
|
// 预览试卷
|
||||||
const previewExam = () => {
|
const previewExam = () => {
|
||||||
|
@ -7,28 +7,29 @@
|
|||||||
<n-button ghost>导入</n-button>
|
<n-button ghost>导入</n-button>
|
||||||
<n-button ghost>导出</n-button>
|
<n-button ghost>导出</n-button>
|
||||||
<n-button type="error" ghost>删除</n-button>
|
<n-button type="error" ghost>删除</n-button>
|
||||||
<n-input placeholder="请输入想要搜索的内容" />
|
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" @keyup.enter="handleSearch" />
|
||||||
<n-button type="primary">搜索</n-button>
|
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-data-table :columns="columns" :data="examData" :row-key="(row: Exam) => row.id"
|
<n-data-table :columns="columns" :data="examData" :loading="loading" :row-key="(row: Exam) => row.id"
|
||||||
@update:checked-row-keys="handleCheck" class="exam-table" :single-line="false"
|
@update:checked-row-keys="handleCheck" class="exam-table" :single-line="false"
|
||||||
:pagination="paginationConfig" />
|
:pagination="paginationConfig" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, ref, VNode, computed } from 'vue';
|
import { h, ref, VNode, computed, onMounted } from 'vue';
|
||||||
import { NButton, NSpace, useMessage, NDataTable, NInput } from 'naive-ui';
|
import { NButton, NSpace, useMessage, NDataTable, NInput } from 'naive-ui';
|
||||||
import type { DataTableColumns } from 'naive-ui';
|
import type { DataTableColumns } from 'naive-ui';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { ExamApi } from '@/api/modules/exam';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
// 定义考试条目的数据类型
|
// 定义考试条目的数据类型
|
||||||
type Exam = {
|
type Exam = {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
category: '练习' | '考试';
|
category: '练习' | '考试';
|
||||||
questionCount: number;
|
questionCount: number;
|
||||||
@ -44,6 +45,17 @@ type Exam = {
|
|||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const loading = ref(false);
|
||||||
|
const examData = ref<Exam[]>([]);
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const filters = ref({
|
||||||
|
category: '',
|
||||||
|
status: '',
|
||||||
|
difficulty: '',
|
||||||
|
creator: ''
|
||||||
|
});
|
||||||
|
|
||||||
// 创建表格列的函数
|
// 创建表格列的函数
|
||||||
const createColumns = ({
|
const createColumns = ({
|
||||||
handleAction,
|
handleAction,
|
||||||
@ -133,20 +145,119 @@ const createColumns = ({
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模拟数据
|
// 加载试卷列表
|
||||||
const examData = ref<Exam[]>([
|
const loadExamPaperList = async () => {
|
||||||
{ id: 1, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
loading.value = true;
|
||||||
{ id: 2, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '未发布', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
try {
|
||||||
{ id: 3, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
const params: any = {
|
||||||
{ id: 4, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
page: currentPage.value,
|
||||||
{ id: 5, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
pageSize: pageSize.value
|
||||||
{ id: 6, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '未发布', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
};
|
||||||
{ id: 7, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
|
||||||
{ id: 8, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '未发布', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },{ id: 7, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
if (searchKeyword.value) {
|
||||||
{ id: 9, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '未发布', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
params.keyword = searchKeyword.value;
|
||||||
{ id: 10, name: '试卷名称试卷名称', category: '练习', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '发布中', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
}
|
||||||
{ id: 11, name: '试卷名称试卷名称', category: '考试', questionCount: 100, chapter: '第一节 开课前准备', totalScore: 150, difficulty: '易', status: '未发布', startTime: '2025.07.25 09:20', endTime: '2025.07.25 09:20', creator: '王建国', creationTime: '2025.07.25 9:20' },
|
if (filters.value.category) {
|
||||||
]);
|
params.category = filters.value.category;
|
||||||
|
}
|
||||||
|
if (filters.value.status) {
|
||||||
|
params.status = filters.value.status;
|
||||||
|
}
|
||||||
|
if (filters.value.difficulty) {
|
||||||
|
params.difficulty = filters.value.difficulty;
|
||||||
|
}
|
||||||
|
if (filters.value.creator) {
|
||||||
|
params.creator = filters.value.creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔍 获取试卷列表参数:', params);
|
||||||
|
const response = await ExamApi.getExamPaperList(params);
|
||||||
|
console.log('✅ 获取试卷列表成功:', response);
|
||||||
|
|
||||||
|
let listData: any[] = [];
|
||||||
|
let totalCount = 0;
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
const data = response.data as any;
|
||||||
|
if (data.result) {
|
||||||
|
// API返回的数据结构是 result.records
|
||||||
|
listData = data.result.records || [];
|
||||||
|
totalCount = data.result.total || 0;
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
listData = data;
|
||||||
|
totalCount = data.length;
|
||||||
|
} else if (data.list) {
|
||||||
|
listData = data.list;
|
||||||
|
totalCount = data.total || data.totalCount || 0;
|
||||||
|
} else if (data.records) {
|
||||||
|
listData = data.records;
|
||||||
|
totalCount = data.total || data.totalCount || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(listData)) {
|
||||||
|
console.warn('API返回的数据不是数组格式:', listData);
|
||||||
|
listData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据映射
|
||||||
|
const mappedList = listData.map((item: any) => {
|
||||||
|
const statusMap: { [key: number]: string } = {
|
||||||
|
0: '未发布',
|
||||||
|
1: '发布中',
|
||||||
|
2: '已结束'
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryMap: { [key: number]: string } = {
|
||||||
|
0: '练习',
|
||||||
|
1: '考试'
|
||||||
|
};
|
||||||
|
|
||||||
|
const difficultyMap: { [key: number]: string } = {
|
||||||
|
0: '易',
|
||||||
|
1: '中',
|
||||||
|
2: '难'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间显示
|
||||||
|
const formatTime = (startTime: string, endTime: string) => {
|
||||||
|
if (startTime && endTime) {
|
||||||
|
return `${startTime} - ${endTime}`;
|
||||||
|
} else if (startTime) {
|
||||||
|
return startTime;
|
||||||
|
} else if (endTime) {
|
||||||
|
return endTime;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id || item.paperId || '',
|
||||||
|
name: item.name || '未命名试卷',
|
||||||
|
category: (categoryMap[item.type] || '练习') as '练习' | '考试',
|
||||||
|
questionCount: 0, // 暂时设为0,需要从题目关联表获取
|
||||||
|
chapter: '未分类', // 暂时设为默认值
|
||||||
|
totalScore: 0, // 暂时设为0,需要从题目关联表计算
|
||||||
|
difficulty: (difficultyMap[item.difficulty] || '易') as '易' | '中' | '难',
|
||||||
|
status: (statusMap[item.status] || '未发布') as '发布中' | '未发布' | '已结束',
|
||||||
|
startTime: formatTime(item.startTime, item.endTime),
|
||||||
|
endTime: item.endTime || '',
|
||||||
|
creator: item.createBy || '未知',
|
||||||
|
creationTime: item.createTime || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
examData.value = mappedList;
|
||||||
|
totalItems.value = totalCount;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载试卷列表失败:', error);
|
||||||
|
message.error('加载试卷列表失败');
|
||||||
|
examData.value = [];
|
||||||
|
totalItems.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns = createColumns({
|
const columns = createColumns({
|
||||||
handleAction: (action, row) => {
|
handleAction: (action, row) => {
|
||||||
@ -195,7 +306,7 @@ const handleCheck = (rowKeys: Array<string | number>) => {
|
|||||||
// 分页状态
|
// 分页状态
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const pageSize = ref(10);
|
const pageSize = ref(10);
|
||||||
const totalItems = ref(examData.value.length);
|
const totalItems = ref(0);
|
||||||
|
|
||||||
// 表格分页配置
|
// 表格分页配置
|
||||||
const paginationConfig = computed(() => ({
|
const paginationConfig = computed(() => ({
|
||||||
@ -211,13 +322,22 @@ const paginationConfig = computed(() => ({
|
|||||||
},
|
},
|
||||||
onUpdatePage: (page: number) => {
|
onUpdatePage: (page: number) => {
|
||||||
currentPage.value = page;
|
currentPage.value = page;
|
||||||
|
loadExamPaperList();
|
||||||
},
|
},
|
||||||
onUpdatePageSize: (newPageSize: number) => {
|
onUpdatePageSize: (newPageSize: number) => {
|
||||||
pageSize.value = newPageSize;
|
pageSize.value = newPageSize;
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
|
loadExamPaperList();
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 搜索功能
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
loadExamPaperList();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleAddExam = () => {
|
const handleAddExam = () => {
|
||||||
// 根据当前路由上下文决定跳转路径
|
// 根据当前路由上下文决定跳转路径
|
||||||
const currentRoute = route.path;
|
const currentRoute = route.path;
|
||||||
@ -231,6 +351,11 @@ const handleAddExam = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadExamPaperList();
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -2,12 +2,7 @@
|
|||||||
<div class="marking-center">
|
<div class="marking-center">
|
||||||
<!-- Tab切换 -->
|
<!-- Tab切换 -->
|
||||||
<div class="tab-container">
|
<div class="tab-container">
|
||||||
<n-tabs
|
<n-tabs v-model:value="activeTab" type="line" animated @update:value="handleTabChange">
|
||||||
v-model:value="activeTab"
|
|
||||||
type="line"
|
|
||||||
animated
|
|
||||||
@update:value="handleTabChange"
|
|
||||||
>
|
|
||||||
<n-tab-pane name="all" tab="全部">
|
<n-tab-pane name="all" tab="全部">
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="not-started" tab="未开始">
|
<n-tab-pane name="not-started" tab="未开始">
|
||||||
@ -21,38 +16,31 @@
|
|||||||
|
|
||||||
<!-- 筛选栏 -->
|
<!-- 筛选栏 -->
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
|
<div class="filter-left">
|
||||||
|
<n-input v-model:value="searchKeyword" placeholder="搜索考试名称" clearable style="width: 200px; margin-right: 16px;"
|
||||||
|
@keyup.enter="handleSearch">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="SearchOutline" />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||||
|
</div>
|
||||||
<div class="filter-right">
|
<div class="filter-right">
|
||||||
<n-select
|
<n-select v-model:value="examFilter" :options="examFilterOptions" placeholder="考试类型"
|
||||||
v-model:value="examFilter"
|
style="width: 120px; margin-right: 16px;" clearable />
|
||||||
:options="examFilterOptions"
|
<n-select v-model:value="gradeFilter" :options="gradeFilterOptions" placeholder="班级名称" style="width: 120px;"
|
||||||
placeholder="考试"
|
clearable />
|
||||||
style="width: 120px; margin-right: 16px;"
|
|
||||||
/>
|
|
||||||
<n-select
|
|
||||||
v-model:value="gradeFilter"
|
|
||||||
:options="gradeFilterOptions"
|
|
||||||
placeholder="班级名称"
|
|
||||||
style="width: 120px;"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 试卷列表 -->
|
<!-- 试卷列表 -->
|
||||||
<div class="exam-list">
|
<div class="exam-list">
|
||||||
<div
|
<n-spin :show="loading">
|
||||||
v-for="exam in filteredExams"
|
<div v-for="exam in filteredExams" :key="exam.id" class="exam-item"
|
||||||
:key="exam.id"
|
:class="{ 'completed': exam.status === 'completed' }">
|
||||||
class="exam-item"
|
|
||||||
:class="{ 'completed': exam.status === 'completed' }"
|
|
||||||
>
|
|
||||||
<div class="exam-content">
|
<div class="exam-content">
|
||||||
<div class="exam-header">
|
<div class="exam-header">
|
||||||
<n-tag
|
<n-tag :type="getStatusType(exam.status)" :bordered="false" size="small" class="status-tag">
|
||||||
:type="getStatusType(exam.status)"
|
|
||||||
:bordered="false"
|
|
||||||
size="small"
|
|
||||||
class="status-tag"
|
|
||||||
>
|
|
||||||
{{ getStatusText(exam.status) }}
|
{{ getStatusText(exam.status) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<span class="exam-title">{{ exam.title }}</span>
|
<span class="exam-title">{{ exam.title }}</span>
|
||||||
@ -75,18 +63,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="exam-actions">
|
<div class="exam-actions">
|
||||||
<n-button
|
<n-button text type="primary" @click="handleViewDetails(exam)">
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
@click="handleViewDetails(exam)"
|
|
||||||
>
|
|
||||||
试卷设置
|
试卷设置
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button text type="primary" @click="handleDelete(exam)">
|
||||||
text
|
|
||||||
type="primary"
|
|
||||||
@click="handleDelete(exam)"
|
|
||||||
>
|
|
||||||
删除
|
删除
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
@ -102,41 +82,34 @@
|
|||||||
<div class="stats-label">已交</div>
|
<div class="stats-label">已交</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-item">
|
<div class="stats-item">
|
||||||
<div class="stats-number">{{ exam.gradedCount }}</div>
|
<div class="stats-number">{{ exam.submittedCount - exam.gradedCount }}</div>
|
||||||
<div class="stats-label">{{ exam.status === 'in-progress' ? '0未交' : '0未交' }}</div>
|
<div class="stats-label">未批</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="exam-action-button">
|
<div class="exam-action-button">
|
||||||
<n-button
|
<n-button :type="exam.status === 'completed' ? 'default' : 'primary'" @click="handleAction(exam)">
|
||||||
:type="exam.status === 'completed' ? 'default' : 'primary'"
|
|
||||||
@click="handleAction(exam)"
|
|
||||||
>
|
|
||||||
{{ exam.status === 'completed' ? '查看' : (exam.status === 'in-progress' ? '批阅' : '查看') }}
|
{{ exam.status === 'completed' ? '查看' : (exam.status === 'in-progress' ? '批阅' : '查看') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</n-spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<n-pagination
|
<n-pagination v-model:page="currentPage" :page-size="pageSize" show-size-picker :page-sizes="[10, 20, 50]"
|
||||||
v-model:page="currentPage"
|
:item-count="totalItems" @update:page="handlePageChange" @update:page-size="handlePageSizeChange" />
|
||||||
:page-size="pageSize"
|
|
||||||
show-size-picker
|
|
||||||
:page-sizes="[10, 20, 50]"
|
|
||||||
:item-count="totalItems"
|
|
||||||
@update:page="handlePageChange"
|
|
||||||
@update:page-size="handlePageSizeChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { PersonOutline, CalendarOutline } from '@vicons/ionicons5'
|
import { PersonOutline, CalendarOutline, SearchOutline } from '@vicons/ionicons5'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { ExamApi } from '@/api/modules/exam'
|
||||||
|
|
||||||
// 接口定义
|
// 接口定义
|
||||||
interface ExamItem {
|
interface ExamItem {
|
||||||
@ -149,18 +122,24 @@ interface ExamItem {
|
|||||||
totalQuestions: number
|
totalQuestions: number
|
||||||
submittedCount: number
|
submittedCount: number
|
||||||
gradedCount: number
|
gradedCount: number
|
||||||
|
examType?: string
|
||||||
|
paperId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 路由
|
// 路由
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
const activeTab = ref('all')
|
const activeTab = ref('all')
|
||||||
const examFilter = ref('')
|
const examFilter = ref('')
|
||||||
const gradeFilter = ref('')
|
const gradeFilter = ref('')
|
||||||
|
const searchKeyword = ref('')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
|
const totalItems = ref(0)
|
||||||
|
|
||||||
// 选项数据
|
// 选项数据
|
||||||
const examFilterOptions = [
|
const examFilterOptions = [
|
||||||
@ -170,78 +149,231 @@ const examFilterOptions = [
|
|||||||
{ label: '月考', value: 'monthly' }
|
{ label: '月考', value: 'monthly' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const gradeFilterOptions = [
|
const gradeFilterOptions = ref([
|
||||||
{ label: '全部班级', value: '' },
|
{ label: '全部班级', value: '' }
|
||||||
{ label: '一年级1班', value: 'grade1-1' },
|
|
||||||
{ label: '一年级2班', value: 'grade1-2' },
|
|
||||||
{ label: '二年级1班', value: 'grade2-1' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 模拟数据
|
|
||||||
const examList = ref<ExamItem[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: '试卷名称试卷名称试卷名称试卷名称试卷名称',
|
|
||||||
description: '试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明...',
|
|
||||||
creator: 'xx',
|
|
||||||
duration: '考试时间:2025.6.18-2025.9.18',
|
|
||||||
status: 'not-started',
|
|
||||||
totalQuestions: 10,
|
|
||||||
submittedCount: 0,
|
|
||||||
gradedCount: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: '试卷名称试卷名称试卷名称试卷名称试卷名称',
|
|
||||||
description: '试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明...',
|
|
||||||
creator: 'xx',
|
|
||||||
duration: '考试时间:2025.6.18-2025.9.18',
|
|
||||||
status: 'in-progress',
|
|
||||||
totalQuestions: 0,
|
|
||||||
submittedCount: 0,
|
|
||||||
gradedCount: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: '试卷名称试卷名称试卷名称试卷名称试卷名称',
|
|
||||||
description: '试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明试卷说明...',
|
|
||||||
creator: 'xx',
|
|
||||||
duration: '考试时间:2025.6.18-2025.9.18',
|
|
||||||
status: 'completed',
|
|
||||||
totalQuestions: 10,
|
|
||||||
submittedCount: 0,
|
|
||||||
gradedCount: 0
|
|
||||||
}
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// 计算属性
|
// 考试列表数据
|
||||||
const filteredExams = computed(() => {
|
const examList = ref<ExamItem[]>([])
|
||||||
let filtered = examList.value
|
|
||||||
|
|
||||||
// 根据tab过滤
|
// 加载班级列表
|
||||||
|
const loadClassList = async () => {
|
||||||
|
try {
|
||||||
|
const response = await ExamApi.getClassList()
|
||||||
|
console.log('班级列表API响应:', response)
|
||||||
|
|
||||||
|
// 处理不同的响应数据结构
|
||||||
|
let classList: any[] = []
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
const data = response.data as any
|
||||||
|
|
||||||
|
// 检查是否有result.records字段(分页数据)
|
||||||
|
if (data.result && data.result.records) {
|
||||||
|
classList = data.result.records
|
||||||
|
}
|
||||||
|
// 检查是否有result字段且为数组
|
||||||
|
else if (data.result && Array.isArray(data.result)) {
|
||||||
|
classList = data.result
|
||||||
|
}
|
||||||
|
// 检查是否直接是数组
|
||||||
|
else if (Array.isArray(data)) {
|
||||||
|
classList = data
|
||||||
|
}
|
||||||
|
// 检查是否有records字段
|
||||||
|
else if (data.records && Array.isArray(data.records)) {
|
||||||
|
classList = data.records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('解析后的班级列表:', classList)
|
||||||
|
|
||||||
|
if (Array.isArray(classList)) {
|
||||||
|
gradeFilterOptions.value = [
|
||||||
|
{ label: '全部班级', value: '' },
|
||||||
|
...classList.map((item: any) => ({
|
||||||
|
label: item.name || '未命名班级',
|
||||||
|
value: item.id
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
console.log('班级筛选选项:', gradeFilterOptions.value)
|
||||||
|
} else {
|
||||||
|
console.warn('班级列表数据格式不正确:', classList)
|
||||||
|
gradeFilterOptions.value = [{ label: '全部班级', value: '' }]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载班级列表失败:', error)
|
||||||
|
message.error('加载班级列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载阅卷列表
|
||||||
|
const loadMarkingList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 先测试基本参数,逐步添加筛选条件
|
||||||
|
const params: any = {
|
||||||
|
page: currentPage.value,
|
||||||
|
pageSize: pageSize.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加状态筛选
|
||||||
if (activeTab.value !== 'all') {
|
if (activeTab.value !== 'all') {
|
||||||
filtered = filtered.filter(exam => exam.status === activeTab.value)
|
params.status = activeTab.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据考试类型过滤
|
// 添加其他筛选条件(如果后端支持)
|
||||||
if (examFilter.value) {
|
if (examFilter.value) {
|
||||||
// 这里可以添加具体的过滤逻辑
|
params.examType = examFilter.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据班级过滤
|
|
||||||
if (gradeFilter.value) {
|
if (gradeFilter.value) {
|
||||||
// 这里可以添加具体的过滤逻辑
|
params.className = gradeFilter.value
|
||||||
|
}
|
||||||
|
if (searchKeyword.value) {
|
||||||
|
params.keyword = searchKeyword.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
console.log('🔍 筛选参数:', params)
|
||||||
|
console.log('📊 当前筛选状态:', {
|
||||||
|
activeTab: activeTab.value,
|
||||||
|
examFilter: examFilter.value,
|
||||||
|
gradeFilter: gradeFilter.value,
|
||||||
|
searchKeyword: searchKeyword.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalItems = computed(() => filteredExams.value.length)
|
const response = await ExamApi.getMarkingList(params)
|
||||||
|
console.log('阅卷列表API响应:', response)
|
||||||
|
|
||||||
|
// 处理不同的响应数据结构
|
||||||
|
let listData: any[] = []
|
||||||
|
let totalCount = 0
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
const data = response.data as any
|
||||||
|
|
||||||
|
// 检查是否有result字段
|
||||||
|
if (data.result) {
|
||||||
|
listData = data.result.list || data.result.records || []
|
||||||
|
totalCount = data.result.total || data.result.totalCount || 0
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
|
// 如果直接返回数组
|
||||||
|
listData = data
|
||||||
|
totalCount = data.length
|
||||||
|
} else if (data.list) {
|
||||||
|
// 如果有list字段
|
||||||
|
listData = data.list
|
||||||
|
totalCount = data.total || data.totalCount || 0
|
||||||
|
} else if (data.records) {
|
||||||
|
// 如果有records字段
|
||||||
|
listData = data.records
|
||||||
|
totalCount = data.total || data.totalCount || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保listData是数组
|
||||||
|
if (!Array.isArray(listData)) {
|
||||||
|
console.warn('API返回的数据不是数组格式:', listData)
|
||||||
|
listData = []
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappedList = listData.map((item: any) => {
|
||||||
|
// 根据API返回的数据结构进行映射
|
||||||
|
const statusMap: { [key: number]: string } = {
|
||||||
|
0: 'not-started', // 未开始
|
||||||
|
1: 'in-progress', // 进行中
|
||||||
|
2: 'completed' // 已完成
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeMap: { [key: number]: string } = {
|
||||||
|
0: '期中考试',
|
||||||
|
1: '期末考试',
|
||||||
|
2: '月考',
|
||||||
|
3: '随堂测验'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间显示
|
||||||
|
const formatDuration = (startTime: string, endTime: string, totalTime: number) => {
|
||||||
|
if (startTime && endTime) {
|
||||||
|
const start = new Date(startTime).toLocaleString('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
const end = new Date(endTime).toLocaleString('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
return `${start} - ${end}`
|
||||||
|
}
|
||||||
|
return totalTime ? `${totalTime}分钟` : '未设置'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: item.name || '未命名考试',
|
||||||
|
description: item.description || item.remark || '',
|
||||||
|
creator: item.createBy || '未知',
|
||||||
|
duration: formatDuration(item.startTime, item.endTime, item.totalTime),
|
||||||
|
status: (statusMap[item.status] || 'not-started') as 'not-started' | 'in-progress' | 'completed',
|
||||||
|
totalQuestions: item.totalQuestions || 0,
|
||||||
|
submittedCount: item.submittedCount || 0,
|
||||||
|
gradedCount: item.gradedCount || 0,
|
||||||
|
// 添加额外信息用于显示
|
||||||
|
examType: typeMap[item.type] || '未知类型',
|
||||||
|
paperId: item.paperId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果后端不支持筛选,在前端进行筛选
|
||||||
|
if (activeTab.value !== 'all' || examFilter.value || gradeFilter.value || searchKeyword.value) {
|
||||||
|
console.log('🔍 后端可能不支持筛选,在前端进行筛选')
|
||||||
|
mappedList = mappedList.filter(item => {
|
||||||
|
// 状态筛选
|
||||||
|
if (activeTab.value !== 'all' && item.status !== activeTab.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 考试类型筛选
|
||||||
|
if (examFilter.value && item.examType !== examFilter.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 班级筛选(这里需要根据实际数据结构调整)
|
||||||
|
if (gradeFilter.value) {
|
||||||
|
// 暂时跳过班级筛选,因为需要更多信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (searchKeyword.value && !item.title.toLowerCase().includes(searchKeyword.value.toLowerCase())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
examList.value = mappedList
|
||||||
|
|
||||||
|
totalItems.value = totalCount
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载阅卷列表失败:', error)
|
||||||
|
message.error('加载阅卷列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const filteredExams = computed(() => examList.value)
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
activeTab.value = value
|
activeTab.value = value
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
loadMarkingList()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusType = (status: string) => {
|
const getStatusType = (status: string) => {
|
||||||
@ -296,15 +428,34 @@ const handleAction = (exam: ExamItem) => {
|
|||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
|
loadMarkingList()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageSizeChange = (size: number) => {
|
const handlePageSizeChange = (size: number) => {
|
||||||
pageSize.value = size
|
pageSize.value = size
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
loadMarkingList()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
loadMarkingList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听筛选条件变化
|
||||||
|
watch([activeTab, examFilter, gradeFilter, searchKeyword], () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
loadMarkingList()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
// 组件挂载后的初始化操作
|
// 组件挂载后的初始化操作
|
||||||
|
await Promise.all([
|
||||||
|
loadClassList(),
|
||||||
|
loadMarkingList()
|
||||||
|
])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -333,13 +484,17 @@ onMounted(() => {
|
|||||||
/* 筛选栏样式 */
|
/* 筛选栏样式 */
|
||||||
.filter-container {
|
.filter-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.filter-right {
|
.filter-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -10,11 +10,7 @@
|
|||||||
<n-button @click="exportChapters">导出</n-button>
|
<n-button @click="exportChapters">导出</n-button>
|
||||||
<n-button type="error" :disabled="selectedChapters.length === 0" @click="deleteSelected">删除</n-button>
|
<n-button type="error" :disabled="selectedChapters.length === 0" @click="deleteSelected">删除</n-button>
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<n-input
|
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" style="width: 200px;">
|
||||||
v-model:value="searchKeyword"
|
|
||||||
placeholder="请输入想要搜索的内容"
|
|
||||||
style="width: 200px;"
|
|
||||||
>
|
|
||||||
</n-input>
|
</n-input>
|
||||||
<n-button type="primary" @click="searchChapters">搜索</n-button>
|
<n-button type="primary" @click="searchChapters">搜索</n-button>
|
||||||
</div>
|
</div>
|
||||||
@ -24,9 +20,9 @@
|
|||||||
|
|
||||||
<!-- 章节列表表格 -->
|
<!-- 章节列表表格 -->
|
||||||
<div class="table-box">
|
<div class="table-box">
|
||||||
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey"
|
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey" :checked-row-keys="selectedChapters"
|
||||||
:checked-row-keys="selectedChapters" @update:checked-row-keys="handleCheck" :bordered="false"
|
@update:checked-row-keys="handleCheck" :bordered="false" :single-line="false" size="medium"
|
||||||
:single-line="false" size="medium" class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" />
|
class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" :loading="loading" />
|
||||||
|
|
||||||
<!-- 自定义分页器 -->
|
<!-- 自定义分页器 -->
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
@ -61,31 +57,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ImportModal
|
<ImportModal v-model:show="showImportModal" template-name="custom_template.xlsx" import-type="custom"
|
||||||
v-model:show="showImportModal"
|
@success="handleImportSuccess" @template-download="handleTemplateDownload" />
|
||||||
template-name="custom_template.xlsx"
|
|
||||||
import-type="custom"
|
|
||||||
@success="handleImportSuccess"
|
|
||||||
@template-download="handleTemplateDownload" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, h } from 'vue'
|
import { ref, computed, h, onMounted } from 'vue'
|
||||||
import { NButton, useMessage, NDataTable, NInput, NSpace } from 'naive-ui'
|
import { NButton, useMessage, NDataTable, NInput, NSpace, useDialog } from 'naive-ui'
|
||||||
import type { DataTableColumns } from 'naive-ui'
|
import type { DataTableColumns } from 'naive-ui'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import ImportModal from '@/components/common/ImportModal.vue'
|
import ImportModal from '@/components/common/ImportModal.vue'
|
||||||
|
import { ChapterApi } from '@/api'
|
||||||
|
import type { ChapterQueryParams } from '@/api/types'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
// 章节类型定义
|
// 章节类型定义
|
||||||
interface Chapter {
|
interface Chapter {
|
||||||
id: number
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
sort: string | number
|
sort: string | number
|
||||||
@ -93,157 +88,155 @@ interface Chapter {
|
|||||||
isParent: boolean
|
isParent: boolean
|
||||||
children?: Chapter[]
|
children?: Chapter[]
|
||||||
expanded?: boolean
|
expanded?: boolean
|
||||||
|
level?: number
|
||||||
|
parentId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const showImportModal = ref(false)
|
const showImportModal = ref(false)
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
// 获取当前用户ID作为courseId参数
|
||||||
|
const courseId = computed(() => userStore.user?.id?.toString() || '')
|
||||||
|
|
||||||
const handleImportSuccess = () => {
|
const handleImportSuccess = () => {
|
||||||
message.success('章节导入成功')
|
message.success('章节导入成功')
|
||||||
|
// 重新加载章节列表
|
||||||
|
loadChapters()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTemplateDownload = () => {
|
const handleTemplateDownload = () => {
|
||||||
message.success('模板下载成功')
|
message.success('模板下载成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索关键词
|
// 搜索关键词
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
// 选中的章节
|
// 选中的章节
|
||||||
const selectedChapters = ref<number[]>([])
|
const selectedChapters = ref<string[]>([])
|
||||||
|
|
||||||
// 章节列表数据
|
// 章节列表数据
|
||||||
const chapterList = ref<Chapter[]>([
|
const chapterList = ref<Chapter[]>([])
|
||||||
{
|
|
||||||
id: 1,
|
// 原始章节数据(用于搜索过滤)
|
||||||
name: '第一章 课前准备',
|
const originalChapterList = ref<Chapter[]>([])
|
||||||
type: '-',
|
|
||||||
sort: '-',
|
// 加载章节数据
|
||||||
createTime: '2025.07.25 09:20',
|
const loadChapters = async () => {
|
||||||
isParent: true,
|
if (!courseId.value) {
|
||||||
expanded: false,
|
message.error('用户未登录,无法获取章节数据')
|
||||||
children: [
|
return
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: '开课彩蛋:新开始新征程',
|
|
||||||
type: '视频',
|
|
||||||
sort: 1,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '课件准备PPT',
|
|
||||||
type: '课件',
|
|
||||||
sort: 2,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: '第一节 课程定位与目标',
|
|
||||||
type: '视频',
|
|
||||||
sort: 3,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
name: '第二节 教学安排及学习建议',
|
|
||||||
type: '作业',
|
|
||||||
sort: 4,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: '第三节 教学安排及学习建议',
|
|
||||||
type: '考试',
|
|
||||||
sort: 5,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
try {
|
||||||
{
|
loading.value = true
|
||||||
id: 7,
|
error.value = ''
|
||||||
name: '第二章 课前准备',
|
|
||||||
type: '-',
|
const params: ChapterQueryParams = {
|
||||||
sort: '-',
|
courseId: courseId.value,
|
||||||
createTime: '2025.07.25 09:20',
|
page: 1,
|
||||||
isParent: true,
|
pageSize: 1000 // 获取所有章节
|
||||||
expanded: false,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
id: 8,
|
|
||||||
name: '第一节 新开始新征程',
|
|
||||||
type: '视频',
|
|
||||||
sort: 1,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 9,
|
|
||||||
name: '第二节 教学安排及学习建议',
|
|
||||||
type: '课件',
|
|
||||||
sort: 2,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
const response = await ChapterApi.getChapters(params)
|
||||||
{
|
|
||||||
id: 10,
|
console.log('🔍 完整API响应:', response)
|
||||||
name: '第三章 课前准备',
|
console.log('🔍 response.data类型:', typeof response.data)
|
||||||
type: '-',
|
console.log('🔍 response.data是否为数组:', Array.isArray(response.data))
|
||||||
sort: '-',
|
console.log('🔍 response.data内容:', response.data)
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: true,
|
if (response.code === 200 && response.data) {
|
||||||
expanded: false,
|
// 检查response.data是否有list属性
|
||||||
children: [
|
if (response.data.list && Array.isArray(response.data.list)) {
|
||||||
{
|
// 转换API数据为组件需要的格式
|
||||||
id: 12,
|
const chapters = convertApiDataToChapter(response.data.list)
|
||||||
name: '第一节 新开始新征程',
|
chapterList.value = chapters
|
||||||
type: '视频',
|
originalChapterList.value = chapters
|
||||||
sort: 1,
|
message.success('章节加载成功')
|
||||||
createTime: '2025.07.25 09:20',
|
} else {
|
||||||
isParent: false
|
console.error('❌ response.data.list不是数组:', response.data)
|
||||||
},
|
throw new Error('API返回的数据格式不正确')
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
name: '第二节 教学安排及学习建议',
|
|
||||||
type: '课件',
|
|
||||||
sort: 2,
|
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: false
|
|
||||||
}
|
}
|
||||||
]
|
} else {
|
||||||
},
|
throw new Error(response.message || '获取章节失败')
|
||||||
{
|
}
|
||||||
id: 11,
|
} catch (err: any) {
|
||||||
name: '第四章 课前准备',
|
console.error('加载章节失败:', err)
|
||||||
type: '-',
|
error.value = err.message || '加载章节失败'
|
||||||
sort: '-',
|
message.error(error.value)
|
||||||
createTime: '2025.07.25 09:20',
|
|
||||||
isParent: true,
|
// API调用失败时显示空列表,不使用模拟数据
|
||||||
|
chapterList.value = []
|
||||||
|
originalChapterList.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换API数据为组件格式
|
||||||
|
const convertApiDataToChapter = (apiData: any[]): Chapter[] => {
|
||||||
|
console.log('🔍 convertApiDataToChapter 输入数据:', apiData)
|
||||||
|
console.log('🔍 输入数据类型:', typeof apiData)
|
||||||
|
console.log('🔍 输入数据是否为数组:', Array.isArray(apiData))
|
||||||
|
|
||||||
|
if (!Array.isArray(apiData)) {
|
||||||
|
console.error('❌ apiData不是数组:', apiData)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 sort 排序
|
||||||
|
const sortedData = [...apiData].sort((a, b) => (a.sort || 0) - (b.sort || 0))
|
||||||
|
|
||||||
|
return sortedData.map(section => ({
|
||||||
|
id: section.id,
|
||||||
|
name: section.name,
|
||||||
|
type: mapChapterType(section.type),
|
||||||
|
sort: section.sort,
|
||||||
|
createTime: section.createdAt ? new Date(section.createdAt).toLocaleString() : '',
|
||||||
|
isParent: section.level === 1, // level=1 表示章,为父章节
|
||||||
|
level: section.level,
|
||||||
|
parentId: section.parentId,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
children: []
|
children: []
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
])
|
|
||||||
|
// 映射章节类型 - 根据数据库字段说明:0:视频、1:资料、2:考试、3:作业
|
||||||
|
const mapChapterType = (type: number | null): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 0: return '视频'
|
||||||
|
case 1: return '资料'
|
||||||
|
case 2: return '考试'
|
||||||
|
case 3: return '作业'
|
||||||
|
default: return '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 扁平化章节列表(用于显示和分页)
|
// 扁平化章节列表(用于显示和分页)
|
||||||
const flattenedChapters = computed(() => {
|
const flattenedChapters = computed(() => {
|
||||||
const result: Chapter[] = []
|
const result: Chapter[] = []
|
||||||
|
|
||||||
const flatten = (chapters: Chapter[]) => {
|
// 分离章节(level=1)和节(level=2),过滤掉level=0的项目
|
||||||
|
const chapters = chapterList.value.filter(item => item.level === 1)
|
||||||
|
const sections = chapterList.value.filter(item => item.level === 2)
|
||||||
|
|
||||||
|
// 为每个章节添加其子节
|
||||||
chapters.forEach(chapter => {
|
chapters.forEach(chapter => {
|
||||||
|
chapter.children = sections.filter(section => section.parentId === chapter.id)
|
||||||
result.push(chapter)
|
result.push(chapter)
|
||||||
if (chapter.children && chapter.expanded) {
|
|
||||||
flatten(chapter.children)
|
// 如果章节展开,添加其子节
|
||||||
}
|
if (chapter.expanded && chapter.children) {
|
||||||
|
chapter.children.forEach(section => {
|
||||||
|
result.push(section)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
flatten(chapterList.value)
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -302,7 +295,7 @@ const paginatedChapters = computed(() => {
|
|||||||
const rowKey = (row: Chapter) => row.id
|
const rowKey = (row: Chapter) => row.id
|
||||||
|
|
||||||
// 表格选择处理
|
// 表格选择处理
|
||||||
const handleCheck = (rowKeys: number[]) => {
|
const handleCheck = (rowKeys: string[]) => {
|
||||||
selectedChapters.value = rowKeys
|
selectedChapters.value = rowKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +344,7 @@ const goToPage = (page: string | number) => {
|
|||||||
|
|
||||||
// 展开/收起章节
|
// 展开/收起章节
|
||||||
const toggleChapter = (chapter: Chapter) => {
|
const toggleChapter = (chapter: Chapter) => {
|
||||||
if (chapter.isParent && chapter.children) {
|
if (chapter.level === 1 && chapter.children) {
|
||||||
chapter.expanded = !chapter.expanded
|
chapter.expanded = !chapter.expanded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -367,6 +360,7 @@ const addChapter = () => {
|
|||||||
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
|
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const importChapters = () => {
|
const importChapters = () => {
|
||||||
showImportModal.value = true
|
showImportModal.value = true
|
||||||
}
|
}
|
||||||
@ -375,39 +369,168 @@ const exportChapters = () => {
|
|||||||
message.info('导出章节功能')
|
message.info('导出章节功能')
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteSelected = () => {
|
const deleteSelected = async () => {
|
||||||
if (selectedChapters.value.length === 0) return
|
if (selectedChapters.value.length === 0) return
|
||||||
if (confirm(`确定要删除选中的 ${selectedChapters.value.length} 个章节吗?`)) {
|
|
||||||
selectedChapters.value.forEach((id: number) => {
|
try {
|
||||||
|
if (!userStore.user?.id) {
|
||||||
|
message.error('用户未登录,无法删除章节')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示确认对话框
|
||||||
|
const confirmed = await new Promise<boolean>((resolve) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认批量删除章节',
|
||||||
|
content: `确定要删除选中的 ${selectedChapters.value.length} 个章节吗?删除后无法恢复。`,
|
||||||
|
positiveText: '确认删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveButtonProps: {
|
||||||
|
type: 'error'
|
||||||
|
},
|
||||||
|
onPositiveClick: () => {
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
// 使用批量删除API
|
||||||
|
const response = await ChapterApi.deleteChaptersBatch(selectedChapters.value)
|
||||||
|
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
message.success(`成功删除 ${selectedChapters.value.length} 个章节`)
|
||||||
|
|
||||||
|
// 从列表中移除已删除的章节
|
||||||
|
selectedChapters.value.forEach((id: string) => {
|
||||||
const index = chapterList.value.findIndex((c: Chapter) => c.id === id)
|
const index = chapterList.value.findIndex((c: Chapter) => c.id === id)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
chapterList.value.splice(index, 1)
|
chapterList.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
selectedChapters.value = []
|
selectedChapters.value = []
|
||||||
message.success('删除成功')
|
} else {
|
||||||
|
message.error('批量删除失败:' + (response.data?.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 批量删除章节失败:', error)
|
||||||
|
message.error('批量删除失败:' + (error.message || '网络错误'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchChapters = () => {
|
const searchChapters = async () => {
|
||||||
message.info('搜索章节: ' + searchKeyword.value)
|
if (!courseId.value) {
|
||||||
|
message.error('用户未登录,无法搜索章节')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
const params: ChapterQueryParams = {
|
||||||
|
courseId: courseId.value,
|
||||||
|
keyword: searchKeyword.value,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await ChapterApi.searchChapters(params)
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||||
|
// 转换API数据为组件需要的格式
|
||||||
|
const chapters = convertApiDataToChapter(response.data.list)
|
||||||
|
chapterList.value = chapters
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
|
message.success(`搜索到 ${chapters.length} 个章节`)
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || '搜索章节失败')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('搜索章节失败:', err)
|
||||||
|
error.value = err.message || '搜索章节失败'
|
||||||
|
message.error(error.value)
|
||||||
|
|
||||||
|
// 搜索失败时显示空列表,不使用本地过滤
|
||||||
|
chapterList.value = []
|
||||||
|
currentPage.value = 1
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editChapter = (chapter: Chapter) => {
|
const editChapter = (chapter: Chapter) => {
|
||||||
message.info('编辑章节: ' + chapter.name)
|
console.log('编辑章节:', chapter)
|
||||||
|
// 跳转到章节编辑器页面
|
||||||
|
const courseId = route.params.id
|
||||||
|
if (courseId) {
|
||||||
|
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
|
||||||
|
} else {
|
||||||
|
message.error('课程ID不存在')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteChapter = (chapter: Chapter) => {
|
// 在 setup 函数中初始化 dialog
|
||||||
if (confirm('确定要删除这个章节吗?')) {
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const deleteChapter = async (chapter: Chapter) => {
|
||||||
|
try {
|
||||||
|
if (!userStore.user?.id) {
|
||||||
|
message.error('用户未登录,无法删除章节')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示确认对话框
|
||||||
|
const confirmed = await new Promise<boolean>((resolve) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认删除章节',
|
||||||
|
content: `确定要删除章节"${chapter.name}"吗?删除后无法恢复。`,
|
||||||
|
positiveText: '确认删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveButtonProps: {
|
||||||
|
type: 'error'
|
||||||
|
},
|
||||||
|
onPositiveClick: () => {
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
// 调用删除API
|
||||||
|
const response = await ChapterApi.deleteChapter(chapter.id.toString())
|
||||||
|
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
message.success('章节删除成功!')
|
||||||
|
|
||||||
|
// 从列表中移除已删除的章节
|
||||||
const index = chapterList.value.findIndex((c: Chapter) => c.id === chapter.id)
|
const index = chapterList.value.findIndex((c: Chapter) => c.id === chapter.id)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
chapterList.value.splice(index, 1)
|
chapterList.value.splice(index, 1)
|
||||||
message.success('删除成功')
|
}
|
||||||
}
|
} else {
|
||||||
|
message.error('章节删除失败:' + (response.data?.message || '未知错误'))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('❌ 删除章节失败:', error)
|
||||||
|
message.error('删除章节失败:' + (error.message || '网络错误'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadChapters()
|
||||||
|
})
|
||||||
|
|
||||||
// 表格列配置 - 使用 minWidth 实现响应式
|
// 表格列配置 - 使用 minWidth 实现响应式
|
||||||
const columns: DataTableColumns<Chapter> = [
|
const columns: DataTableColumns<Chapter> = [
|
||||||
{
|
{
|
||||||
@ -423,17 +546,18 @@ const columns: DataTableColumns<Chapter> = [
|
|||||||
tooltip: true
|
tooltip: true
|
||||||
},
|
},
|
||||||
render: (row: Chapter) => {
|
render: (row: Chapter) => {
|
||||||
|
const isChapter = row.level === 1; // level=1 表示章
|
||||||
return h('div', {
|
return h('div', {
|
||||||
style: {
|
style: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '20px',
|
gap: '20px',
|
||||||
cursor: row.isParent ? 'pointer' : 'default',
|
cursor: isChapter ? 'pointer' : 'default',
|
||||||
marginLeft: row.isParent ? '0px' : '-3px'
|
marginLeft: isChapter ? '0px' : '-3px'
|
||||||
},
|
},
|
||||||
onClick: row.isParent ? () => toggleChapter(row) : undefined
|
onClick: isChapter ? () => toggleChapter(row) : undefined
|
||||||
}, [
|
}, [
|
||||||
row.isParent ? h('i', {
|
isChapter ? h('i', {
|
||||||
class: 'n-base-icon',
|
class: 'n-base-icon',
|
||||||
style: {
|
style: {
|
||||||
transition: 'transform 0.2s',
|
transition: 'transform 0.2s',
|
||||||
@ -454,10 +578,10 @@ const columns: DataTableColumns<Chapter> = [
|
|||||||
]) : null,
|
]) : null,
|
||||||
h('span', {
|
h('span', {
|
||||||
style: {
|
style: {
|
||||||
color: row.isParent ? '#062333' : '#666666',
|
color: isChapter ? '#062333' : '#666666',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
fontWeight: row.isParent ? '500' : 'normal',
|
fontWeight: isChapter ? '500' : 'normal',
|
||||||
marginLeft: row.isParent ? '0' : '24px'
|
marginLeft: isChapter ? '0' : '24px'
|
||||||
}
|
}
|
||||||
}, row.name)
|
}, row.name)
|
||||||
])
|
])
|
||||||
@ -468,7 +592,8 @@ const columns: DataTableColumns<Chapter> = [
|
|||||||
key: 'type',
|
key: 'type',
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
render: (row: Chapter) => {
|
render: (row: Chapter) => {
|
||||||
if (row.type === '-') {
|
const isChapter = row.level === 1; // level=1 表示章
|
||||||
|
if (isChapter || row.type === '-') {
|
||||||
return h('span', { style: { color: '#BABABA' } }, '-')
|
return h('span', { style: { color: '#BABABA' } }, '-')
|
||||||
}
|
}
|
||||||
return h('div', {
|
return h('div', {
|
||||||
@ -489,6 +614,10 @@ const columns: DataTableColumns<Chapter> = [
|
|||||||
key: 'sort',
|
key: 'sort',
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
render: (row: Chapter) => {
|
render: (row: Chapter) => {
|
||||||
|
const isChapter = row.level === 1; // level=1 表示章
|
||||||
|
if (isChapter) {
|
||||||
|
return h('span', { style: { color: '#BABABA' } }, '-')
|
||||||
|
}
|
||||||
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.sort)
|
return h('span', { style: { color: '#062333', fontSize: '12px' } }, row.sort)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
194
tatus --porcelain
Normal file
194
tatus --porcelain
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
[1mdiff --git a/src/api/examples/usage.ts b/src/api/examples/usage.ts[m
|
||||||
|
[1mindex 0363f0d..64dbce6 100644[m
|
||||||
|
[1m--- a/src/api/examples/usage.ts[m
|
||||||
|
[1m+++ b/src/api/examples/usage.ts[m
|
||||||
|
[36m@@ -93,13 +93,8 @@[m [mexport const searchCoursesExample = async () => {[m
|
||||||
|
try {[m
|
||||||
|
const response = await CourseApi.searchCourses({[m
|
||||||
|
keyword: 'Vue.js',[m
|
||||||
|
[31m- category: '前端开发',[m
|
||||||
|
[31m- level: 'intermediate',[m
|
||||||
|
[31m- price: 'paid',[m
|
||||||
|
[31m- rating: 4,[m
|
||||||
|
[31m- sortBy: 'newest',[m
|
||||||
|
[31m- page: 1,[m
|
||||||
|
[31m- pageSize: 10[m
|
||||||
|
[32m+[m[32m limit: '20',[m
|
||||||
|
[32m+[m[32m page: 1[m
|
||||||
|
})[m
|
||||||
|
[m
|
||||||
|
if (response.code === 200) {[m
|
||||||
|
[1mdiff --git a/src/api/modules/course.ts b/src/api/modules/course.ts[m
|
||||||
|
[1mindex 9d3a3e1..e8cdde2 100644[m
|
||||||
|
[1m--- a/src/api/modules/course.ts[m
|
||||||
|
[1m+++ b/src/api/modules/course.ts[m
|
||||||
|
[36m@@ -23,7 +23,7 @@[m [mimport type {[m
|
||||||
|
CourseComment,[m
|
||||||
|
Quiz,[m
|
||||||
|
LearningProgress,[m
|
||||||
|
[31m- SearchRequest,[m
|
||||||
|
[32m+[m
|
||||||
|
Instructor,[m
|
||||||
|
} from '../types'[m
|
||||||
|
[m
|
||||||
|
[1mdiff --git a/src/api/modules/exam.ts b/src/api/modules/exam.ts[m
|
||||||
|
[1mindex 5a1865f..f7be93a 100644[m
|
||||||
|
[1m--- a/src/api/modules/exam.ts[m
|
||||||
|
[1m+++ b/src/api/modules/exam.ts[m
|
||||||
|
[36m@@ -333,6 +333,420 @@[m [mexport class ExamApi {[m
|
||||||
|
console.log('✅ 批量添加题目答案成功:', responses)[m
|
||||||
|
return responses[m
|
||||||
|
}[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m // ========== 试卷管理相关接口 ==========[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 获取试卷列表[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async getExamPaperList(params: {[m
|
||||||
|
[32m+[m[32m page?: number[m
|
||||||
|
[32m+[m[32m pageSize?: number[m
|
||||||
|
[32m+[m[32m keyword?: string[m
|
||||||
|
[32m+[m[32m category?: string[m
|
||||||
|
[32m+[m[32m status?: string[m
|
||||||
|
[32m+[m[32m difficulty?: string[m
|
||||||
|
[32m+[m[32m creator?: string[m
|
||||||
|
[32m+[m[32m } = {}): Promise<ApiResponseWithResult<{[m
|
||||||
|
[32m+[m[32m list: any[][m
|
||||||
|
[32m+[m[32m total: number[m
|
||||||
|
[32m+[m[32m page: number[m
|
||||||
|
[32m+[m[32m pageSize: number[m
|
||||||
|
[32m+[m[32m }>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 获取试卷列表:', params)[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.get<{[m
|
||||||
|
[32m+[m[32m result: {[m
|
||||||
|
[32m+[m[32m records: any[][m
|
||||||
|
[32m+[m[32m total: number[m
|
||||||
|
[32m+[m[32m current: number[m
|
||||||
|
[32m+[m[32m size: number[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m[32m }>('/aiol/aiolPaper/list', { params })[m
|
||||||
|
[32m+[m[32m console.log('✅ 获取试卷列表成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 获取试卷详情[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async getExamPaperDetail(id: string): Promise<ApiResponse<any>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 获取试卷详情:', id)[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.get<any>(`/aiol/aiolExam/paperDetail/${id}`)[m
|
||||||
|
[32m+[m[32m console.log('✅ 获取试卷详情成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 创建试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async createExamPaper(data: {[m
|
||||||
|
[32m+[m[32m name: string[m
|
||||||
|
[32m+[m[32m category: string[m
|
||||||
|
[32m+[m[32m description?: string[m
|
||||||
|
[32m+[m[32m totalScore: number[m
|
||||||
|
[32m+[m[32m difficulty: string[m
|
||||||
|
[32m+[m[32m duration: number[m
|
||||||
|
[32m+[m[32m questions: any[][m
|
||||||
|
[32m+[m[32m }): Promise<ApiResponse<string>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 创建试卷:', data)[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.post<string>('/aiol/aiolPaper/add', data)[m
|
||||||
|
[32m+[m[32m console.log('✅ 创建试卷成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 更新试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async updateExamPaper(id: string, data: {[m
|
||||||
|
[32m+[m[32m name?: string[m
|
||||||
|
[32m+[m[32m category?: string[m
|
||||||
|
[32m+[m[32m description?: string[m
|
||||||
|
[32m+[m[32m totalScore?: number[m
|
||||||
|
[32m+[m[32m difficulty?: string[m
|
||||||
|
[32m+[m[32m duration?: number[m
|
||||||
|
[32m+[m[32m questions?: any[][m
|
||||||
|
[32m+[m[32m }): Promise<ApiResponse<string>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 更新试卷:', { id, data })[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.put<string>(`/aiol/aiolExam/paperUpdate/${id}`, data)[m
|
||||||
|
[32m+[m[32m console.log('✅ 更新试卷成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 删除试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async deleteExamPaper(id: string): Promise<ApiResponse<string>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 删除试卷:', id)[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.delete<string>(`/aiol/aiolExam/paperDelete/${id}`)[m
|
||||||
|
[32m+[m[32m console.log('✅ 删除试卷成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 批量删除试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async batchDeleteExamPapers(ids: string[]): Promise<ApiResponse<string>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 批量删除试卷:', ids)[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.post<string>('/aiol/aiolExam/paperBatchDelete', { ids })[m
|
||||||
|
[32m+[m[32m console.log('✅ 批量删除试卷成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 发布试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async publishExamPaper(id: string, data: {[m
|
||||||
|
[32m+[m[32m startTime: string[m
|
||||||
|
[32m+[m[32m endTime: string[m
|
||||||
|
[32m+[m[32m classIds?: string[][m
|
||||||
|
[32m+[m[32m }): Promise<ApiResponse<string>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 发布试卷:', { id, data })[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.post<string>(`/aiol/aiolExam/paperPublish/${id}`, data)[m
|
||||||
|
[32m+[m[32m console.log('✅ 发布试卷成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 取消发布试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async unpublishExamPaper(id: string): Promise<ApiResponse<string>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 取消发布试卷:', id)[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.post<string>(`/aiol/aiolExam/paperUnpublish/${id}`)[m
|
||||||
|
[32m+[m[32m console.log('✅ 取消发布试卷成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 结束试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async endExamPaper(id: string): Promise<ApiResponse<string>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 结束试卷:', id)[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.post<string>(`/aiol/aiolExam/paperEnd/${id}`)[m
|
||||||
|
[32m+[m[32m console.log('✅ 结束试卷成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 导入试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async importExamPaper(file: File): Promise<ApiResponse<string>> {[m
|
||||||
|
[32m+[m[32m console.log('🚀 导入试卷:', file.name)[m
|
||||||
|
[32m+[m[32m const formData = new FormData()[m
|
||||||
|
[32m+[m[32m formData.append('file', file)[m
|
||||||
|
[32m+[m[32m const response = await ApiRequest.post<string>('/aiol/aiolExam/paperImport', formData, {[m
|
||||||
|
[32m+[m[32m headers: {[m
|
||||||
|
[32m+[m[32m 'Content-Type': 'multipart/form-data'[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m[32m })[m
|
||||||
|
[32m+[m[32m console.log('✅ 导入试卷成功:', response)[m
|
||||||
|
[32m+[m[32m return response[m
|
||||||
|
[32m+[m[32m }[m
|
||||||
|
[32m+[m
|
||||||
|
[32m+[m[32m /**[m
|
||||||
|
[32m+[m[32m * 导出试卷[m
|
||||||
|
[32m+[m[32m */[m
|
||||||
|
[32m+[m[32m static async exportE
|
Loading…
x
Reference in New Issue
Block a user