Compare commits

...

2 Commits

10 changed files with 2674 additions and 656 deletions

View File

@ -5,6 +5,7 @@ export * from './request'
// 导出所有API模块
export { default as AuthApi } from './modules/auth'
export { default as CourseApi } from './modules/course'
export { default as ChapterApi } from './modules/chapter'
export { default as CommentApi } from './modules/comment'
export { default as FavoriteApi } from './modules/favorite'
export { default as OrderApi } from './modules/order'

314
src/api/modules/chapter.ts Normal file
View 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

View File

@ -333,6 +333,422 @@ export class ExamApi {
console.log('✅ 批量添加题目答案成功:', 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

View File

@ -462,6 +462,17 @@ export interface BackendCourseSectionListResponse {
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 {
id: string

View File

@ -289,6 +289,7 @@ import TrueFalseQuestion from '@/components/teacher/TrueFalseQuestion.vue';
import FillBlankQuestion from '@/components/teacher/FillBlankQuestion.vue';
import ShortAnswerQuestion from '@/components/teacher/ShortAnswerQuestion.vue';
import CompositeQuestion from '@/components/teacher/CompositeQuestion.vue';
import { ExamApi } from '@/api/modules/exam';
// dialog API
const { dialog } = createDiscreteApi(['dialog'])
@ -998,7 +999,7 @@ const getQuestionTypeFromString = (typeString: string) => {
};
//
const saveExam = () => {
const saveExam = async () => {
//
if (!examForm.title.trim()) {
dialog.warning({
@ -1035,13 +1036,68 @@ const saveExam = () => {
return;
}
//
console.log('保存试卷数据:', examForm);
dialog.success({
title: '保存成功',
content: '试卷保存成功!',
positiveText: '确定'
});
try {
// 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({
title: '保存成功',
content: '试卷保存成功!',
positiveText: '确定',
onPositiveClick: () => {
//
router.back();
}
});
} catch (error) {
console.error('创建试卷失败:', error);
dialog.error({
title: '保存失败',
content: '试卷保存失败,请重试',
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
}))
}));
}
//

View File

@ -7,28 +7,29 @@
<n-button ghost>导入</n-button>
<n-button ghost>导出</n-button>
<n-button type="error" ghost>删除</n-button>
<n-input placeholder="请输入想要搜索的内容" />
<n-button type="primary">搜索</n-button>
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" @keyup.enter="handleSearch" />
<n-button type="primary" @click="handleSearch">搜索</n-button>
</n-space>
</div>
<n-data-table :columns="columns" :data="examData" :row-key="(row: Exam) => row.id"
@update:checked-row-keys="handleCheck" class="exam-table" :single-line="false"
<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"
:pagination="paginationConfig" />
</div>
</template>
<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 type { DataTableColumns } from 'naive-ui';
import { useRouter, useRoute } from 'vue-router';
import { ExamApi } from '@/api/modules/exam';
const router = useRouter();
const route = useRoute();
//
type Exam = {
id: number;
id: string;
name: string;
category: '练习' | '考试';
questionCount: number;
@ -44,6 +45,17 @@ type Exam = {
const message = useMessage();
//
const loading = ref(false);
const examData = ref<Exam[]>([]);
const searchKeyword = ref('');
const filters = ref({
category: '',
status: '',
difficulty: '',
creator: ''
});
//
const createColumns = ({
handleAction,
@ -133,20 +145,119 @@ const createColumns = ({
];
};
//
const examData = ref<Exam[]>([
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
]);
//
const loadExamPaperList = async () => {
loading.value = true;
try {
const params: any = {
page: currentPage.value,
pageSize: pageSize.value
};
if (searchKeyword.value) {
params.keyword = searchKeyword.value;
}
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({
handleAction: (action, row) => {
@ -195,7 +306,7 @@ const handleCheck = (rowKeys: Array<string | number>) => {
//
const currentPage = ref(1);
const pageSize = ref(10);
const totalItems = ref(examData.value.length);
const totalItems = ref(0);
//
const paginationConfig = computed(() => ({
@ -211,13 +322,22 @@ const paginationConfig = computed(() => ({
},
onUpdatePage: (page: number) => {
currentPage.value = page;
loadExamPaperList();
},
onUpdatePageSize: (newPageSize: number) => {
pageSize.value = newPageSize;
currentPage.value = 1;
loadExamPaperList();
}
}));
//
const handleSearch = () => {
currentPage.value = 1;
loadExamPaperList();
};
const handleAddExam = () => {
//
const currentRoute = route.path;
@ -231,6 +351,11 @@ const handleAddExam = () => {
}
};
//
onMounted(() => {
loadExamPaperList();
});
</script>
<style scoped>

View File

@ -2,12 +2,7 @@
<div class="marking-center">
<!-- Tab切换 -->
<div class="tab-container">
<n-tabs
v-model:value="activeTab"
type="line"
animated
@update:value="handleTabChange"
>
<n-tabs v-model:value="activeTab" type="line" animated @update:value="handleTabChange">
<n-tab-pane name="all" tab="全部">
</n-tab-pane>
<n-tab-pane name="not-started" tab="未开始">
@ -21,122 +16,100 @@
<!-- 筛选栏 -->
<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">
<n-select
v-model:value="examFilter"
:options="examFilterOptions"
placeholder="考试"
style="width: 120px; margin-right: 16px;"
/>
<n-select
v-model:value="gradeFilter"
:options="gradeFilterOptions"
placeholder="班级名称"
style="width: 120px;"
/>
<n-select v-model:value="examFilter" :options="examFilterOptions" placeholder="考试类型"
style="width: 120px; margin-right: 16px;" clearable />
<n-select v-model:value="gradeFilter" :options="gradeFilterOptions" placeholder="班级名称" style="width: 120px;"
clearable />
</div>
</div>
<!-- 试卷列表 -->
<div class="exam-list">
<div
v-for="exam in filteredExams"
:key="exam.id"
class="exam-item"
:class="{ 'completed': exam.status === 'completed' }"
>
<div class="exam-content">
<div class="exam-header">
<n-tag
:type="getStatusType(exam.status)"
:bordered="false"
size="small"
class="status-tag"
>
{{ getStatusText(exam.status) }}
</n-tag>
<span class="exam-title">{{ exam.title }}</span>
</div>
<div class="exam-description">
{{ exam.description }}
</div>
<div class="exam-meta">
<div class="meta-item">
<n-icon :component="PersonOutline" />
发布人
<span>{{ exam.creator }}</span>
<n-spin :show="loading">
<div v-for="exam in filteredExams" :key="exam.id" class="exam-item"
:class="{ 'completed': exam.status === 'completed' }">
<div class="exam-content">
<div class="exam-header">
<n-tag :type="getStatusType(exam.status)" :bordered="false" size="small" class="status-tag">
{{ getStatusText(exam.status) }}
</n-tag>
<span class="exam-title">{{ exam.title }}</span>
</div>
<div class="meta-item">
<n-icon :component="CalendarOutline" />
<span>{{ exam.duration }}</span>
<div class="exam-description">
{{ exam.description }}
</div>
<div class="exam-meta">
<div class="meta-item">
<n-icon :component="PersonOutline" />
发布人
<span>{{ exam.creator }}</span>
</div>
<div class="meta-item">
<n-icon :component="CalendarOutline" />
<span>{{ exam.duration }}</span>
</div>
</div>
<div class="exam-actions">
<n-button text type="primary" @click="handleViewDetails(exam)">
试卷设置
</n-button>
<n-button text type="primary" @click="handleDelete(exam)">
删除
</n-button>
</div>
</div>
<div class="exam-actions">
<n-button
text
type="primary"
@click="handleViewDetails(exam)"
>
试卷设置
</n-button>
<n-button
text
type="primary"
@click="handleDelete(exam)"
>
删除
<div class="exam-stats">
<div class="stats-item">
<div class="stats-number">{{ exam.totalQuestions }}</div>
<div class="stats-label">试题</div>
</div>
<div class="stats-item">
<div class="stats-number">{{ exam.submittedCount }}</div>
<div class="stats-label">已交</div>
</div>
<div class="stats-item">
<div class="stats-number">{{ exam.submittedCount - exam.gradedCount }}</div>
<div class="stats-label">未批</div>
</div>
</div>
<div class="exam-action-button">
<n-button :type="exam.status === 'completed' ? 'default' : 'primary'" @click="handleAction(exam)">
{{ exam.status === 'completed' ? '查看' : (exam.status === 'in-progress' ? '批阅' : '查看') }}
</n-button>
</div>
</div>
<div class="exam-stats">
<div class="stats-item">
<div class="stats-number">{{ exam.totalQuestions }}</div>
<div class="stats-label">试题</div>
</div>
<div class="stats-item">
<div class="stats-number">{{ exam.submittedCount }}</div>
<div class="stats-label">已交</div>
</div>
<div class="stats-item">
<div class="stats-number">{{ exam.gradedCount }}</div>
<div class="stats-label">{{ exam.status === 'in-progress' ? '0未交' : '0未交' }}</div>
</div>
</div>
<div class="exam-action-button">
<n-button
:type="exam.status === 'completed' ? 'default' : 'primary'"
@click="handleAction(exam)"
>
{{ exam.status === 'completed' ? '查看' : (exam.status === 'in-progress' ? '批阅' : '查看') }}
</n-button>
</div>
</div>
</n-spin>
</div>
<!-- 分页 -->
<div class="pagination-container">
<n-pagination
v-model:page="currentPage"
:page-size="pageSize"
show-size-picker
:page-sizes="[10, 20, 50]"
:item-count="totalItems"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
/>
<n-pagination v-model:page="currentPage" :page-size="pageSize" show-size-picker :page-sizes="[10, 20, 50]"
:item-count="totalItems" @update:page="handlePageChange" @update:page-size="handlePageSizeChange" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
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 {
@ -149,18 +122,24 @@ interface ExamItem {
totalQuestions: number
submittedCount: number
gradedCount: number
examType?: string
paperId?: string
}
//
const router = useRouter()
const route = useRoute()
const message = useMessage()
//
const loading = ref(false)
const activeTab = ref('all')
const examFilter = ref('')
const gradeFilter = ref('')
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const totalItems = ref(0)
//
const examFilterOptions = [
@ -170,78 +149,231 @@ const examFilterOptions = [
{ label: '月考', value: 'monthly' }
]
const gradeFilterOptions = [
{ 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 gradeFilterOptions = ref([
{ label: '全部班级', value: '' }
])
//
const examList = ref<ExamItem[]>([])
//
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') {
params.status = activeTab.value
}
//
if (examFilter.value) {
params.examType = examFilter.value
}
if (gradeFilter.value) {
params.className = gradeFilter.value
}
if (searchKeyword.value) {
params.keyword = searchKeyword.value
}
console.log('🔍 筛选参数:', params)
console.log('📊 当前筛选状态:', {
activeTab: activeTab.value,
examFilter: examFilter.value,
gradeFilter: gradeFilter.value,
searchKeyword: searchKeyword.value
})
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(() => {
let filtered = examList.value
// tab
if (activeTab.value !== 'all') {
filtered = filtered.filter(exam => exam.status === activeTab.value)
}
//
if (examFilter.value) {
//
}
//
if (gradeFilter.value) {
//
}
return filtered
})
const totalItems = computed(() => filteredExams.value.length)
const filteredExams = computed(() => examList.value)
//
const handleTabChange = (value: string) => {
activeTab.value = value
currentPage.value = 1
loadMarkingList()
}
const getStatusType = (status: string) => {
@ -287,24 +419,43 @@ const handleAction = (exam: ExamItem) => {
router.push(`/teacher/course-editor/${courseId}/practice/review/student-list/${exam.id}`);
} else {
// 使
router.push({
name: 'StudentList',
params: { paperId: exam.id }
router.push({
name: 'StudentList',
params: { paperId: exam.id }
});
}
}
const handlePageChange = (page: number) => {
currentPage.value = page
loadMarkingList()
}
const handlePageSizeChange = (size: number) => {
pageSize.value = size
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>
@ -333,13 +484,17 @@ onMounted(() => {
/* 筛选栏样式 */
.filter-container {
display: flex;
justify-content: flex-end;
justify-content: space-between;
align-items: center;
background-color: #fff;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.filter-left {
display: flex;
align-items: center;
}
.filter-right {
display: flex;
align-items: center;
@ -520,4 +675,4 @@ onMounted(() => {
font-size: 13px;
}
}
</style>
</style>

File diff suppressed because it is too large Load Diff

View File

@ -10,11 +10,7 @@
<n-button @click="exportChapters">导出</n-button>
<n-button type="error" :disabled="selectedChapters.length === 0" @click="deleteSelected">删除</n-button>
<div class="search-container">
<n-input
v-model:value="searchKeyword"
placeholder="请输入想要搜索的内容"
style="width: 200px;"
>
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" style="width: 200px;">
</n-input>
<n-button type="primary" @click="searchChapters">搜索</n-button>
</div>
@ -24,9 +20,9 @@
<!-- 章节列表表格 -->
<div class="table-box">
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey"
:checked-row-keys="selectedChapters" @update:checked-row-keys="handleCheck" :bordered="false"
:single-line="false" size="medium" class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" />
<n-data-table :columns="columns" :data="paginatedChapters" :row-key="rowKey" :checked-row-keys="selectedChapters"
@update:checked-row-keys="handleCheck" :bordered="false" :single-line="false" size="medium"
class="chapter-data-table" :row-class-name="rowClassName" scroll-x="true" :loading="loading" />
<!-- 自定义分页器 -->
<div class="custom-pagination">
@ -56,36 +52,35 @@
<span class="page-number nav-button" :class="{ disabled: currentPage === totalPages }"
@click="goToPage('last')">
尾页
</span>
</span>
</div>
</div>
</div>
</div>
<ImportModal
v-model:show="showImportModal"
template-name="custom_template.xlsx"
import-type="custom"
@success="handleImportSuccess"
@template-download="handleTemplateDownload" />
<ImportModal v-model:show="showImportModal" template-name="custom_template.xlsx" import-type="custom"
@success="handleImportSuccess" @template-download="handleTemplateDownload" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, h } from 'vue'
import { NButton, useMessage, NDataTable, NInput, NSpace } from 'naive-ui'
import { ref, computed, h, onMounted } from 'vue'
import { NButton, useMessage, NDataTable, NInput, NSpace, useDialog } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { useRouter, useRoute } from 'vue-router'
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 route = useRoute()
const userStore = useUserStore()
const message = useMessage()
//
interface Chapter {
id: number
id: string
name: string
type: string
sort: string | number
@ -93,157 +88,155 @@ interface Chapter {
isParent: boolean
children?: Chapter[]
expanded?: boolean
level?: number
parentId?: string
}
const showImportModal = ref(false)
//
const loading = ref(false)
const error = ref('')
// IDcourseId
const courseId = computed(() => userStore.user?.id?.toString() || '')
const handleImportSuccess = () => {
message.success('章节导入成功')
//
loadChapters()
}
const handleTemplateDownload = () => {
message.success('模板下载成功')
}
//
const searchKeyword = ref('')
//
const selectedChapters = ref<number[]>([])
const selectedChapters = ref<string[]>([])
//
const chapterList = ref<Chapter[]>([
{
id: 1,
name: '第一章 课前准备',
type: '-',
sort: '-',
createTime: '2025.07.25 09:20',
isParent: true,
expanded: false,
children: [
{
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
const chapterList = ref<Chapter[]>([])
//
const originalChapterList = ref<Chapter[]>([])
//
const loadChapters = async () => {
if (!courseId.value) {
message.error('用户未登录,无法获取章节数据')
return
}
try {
loading.value = true
error.value = ''
const params: ChapterQueryParams = {
courseId: courseId.value,
page: 1,
pageSize: 1000 //
}
const response = await ChapterApi.getChapters(params)
console.log('🔍 完整API响应:', response)
console.log('🔍 response.data类型:', typeof response.data)
console.log('🔍 response.data是否为数组:', Array.isArray(response.data))
console.log('🔍 response.data内容:', response.data)
if (response.code === 200 && response.data) {
// response.datalist
if (response.data.list && Array.isArray(response.data.list)) {
// API
const chapters = convertApiDataToChapter(response.data.list)
chapterList.value = chapters
originalChapterList.value = chapters
message.success('章节加载成功')
} else {
console.error('❌ response.data.list不是数组:', response.data)
throw new Error('API返回的数据格式不正确')
}
]
},
{
id: 7,
name: '第二章 课前准备',
type: '-',
sort: '-',
createTime: '2025.07.25 09:20',
isParent: true,
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
}
]
},
{
id: 10,
name: '第三章 课前准备',
type: '-',
sort: '-',
createTime: '2025.07.25 09:20',
isParent: true,
expanded: false,
children: [
{
id: 12,
name: '第一节 新开始新征程',
type: '视频',
sort: 1,
createTime: '2025.07.25 09:20',
isParent: false
},
{
id: 13,
name: '第二节 教学安排及学习建议',
type: '课件',
sort: 2,
createTime: '2025.07.25 09:20',
isParent: false
}
]
},
{
id: 11,
name: '第四章 课前准备',
type: '-',
sort: '-',
createTime: '2025.07.25 09:20',
isParent: true,
} else {
throw new Error(response.message || '获取章节失败')
}
} catch (err: any) {
console.error('加载章节失败:', err)
error.value = err.message || '加载章节失败'
message.error(error.value)
// 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,
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 result: Chapter[] = []
const flatten = (chapters: Chapter[]) => {
chapters.forEach(chapter => {
result.push(chapter)
if (chapter.children && chapter.expanded) {
flatten(chapter.children)
}
})
}
// level=1level=2level=0
const chapters = chapterList.value.filter(item => item.level === 1)
const sections = chapterList.value.filter(item => item.level === 2)
//
chapters.forEach(chapter => {
chapter.children = sections.filter(section => section.parentId === chapter.id)
result.push(chapter)
//
if (chapter.expanded && chapter.children) {
chapter.children.forEach(section => {
result.push(section)
})
}
})
flatten(chapterList.value)
return result
})
@ -302,7 +295,7 @@ const paginatedChapters = computed(() => {
const rowKey = (row: Chapter) => row.id
//
const handleCheck = (rowKeys: number[]) => {
const handleCheck = (rowKeys: string[]) => {
selectedChapters.value = rowKeys
}
@ -351,7 +344,7 @@ const goToPage = (page: string | number) => {
// /
const toggleChapter = (chapter: Chapter) => {
if (chapter.isParent && chapter.children) {
if (chapter.level === 1 && chapter.children) {
chapter.expanded = !chapter.expanded
}
}
@ -367,6 +360,7 @@ const addChapter = () => {
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
}
const importChapters = () => {
showImportModal.value = true
}
@ -375,39 +369,168 @@ const exportChapters = () => {
message.info('导出章节功能')
}
const deleteSelected = () => {
const deleteSelected = async () => {
if (selectedChapters.value.length === 0) return
if (confirm(`确定要删除选中的 ${selectedChapters.value.length} 个章节吗?`)) {
selectedChapters.value.forEach((id: number) => {
const index = chapterList.value.findIndex((c: Chapter) => c.id === id)
if (index > -1) {
chapterList.value.splice(index, 1)
}
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)
}
})
})
selectedChapters.value = []
message.success('删除成功')
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)
if (index > -1) {
chapterList.value.splice(index, 1)
}
})
selectedChapters.value = []
} else {
message.error('批量删除失败:' + (response.data?.message || '未知错误'))
}
} catch (error: any) {
console.error('❌ 批量删除章节失败:', error)
message.error('批量删除失败:' + (error.message || '网络错误'))
}
}
const searchChapters = () => {
message.info('搜索章节: ' + searchKeyword.value)
currentPage.value = 1
const searchChapters = async () => {
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
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) => {
message.info('编辑章节: ' + chapter.name)
}
const deleteChapter = (chapter: Chapter) => {
if (confirm('确定要删除这个章节吗?')) {
const index = chapterList.value.findIndex((c: Chapter) => c.id === chapter.id)
if (index > -1) {
chapterList.value.splice(index, 1)
message.success('删除成功')
}
console.log('编辑章节:', chapter)
//
const courseId = route.params.id
if (courseId) {
router.push(`/teacher/chapter-editor-teacher/${courseId}`)
} else {
message.error('课程ID不存在')
}
}
// setup dialog
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)
if (index > -1) {
chapterList.value.splice(index, 1)
}
} else {
message.error('章节删除失败:' + (response.data?.message || '未知错误'))
}
} catch (error: any) {
console.error('❌ 删除章节失败:', error)
message.error('删除章节失败:' + (error.message || '网络错误'))
}
}
//
onMounted(() => {
loadChapters()
})
// - 使 minWidth
const columns: DataTableColumns<Chapter> = [
{
@ -423,17 +546,18 @@ const columns: DataTableColumns<Chapter> = [
tooltip: true
},
render: (row: Chapter) => {
const isChapter = row.level === 1; // level=1
return h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '20px',
cursor: row.isParent ? 'pointer' : 'default',
marginLeft: row.isParent ? '0px' : '-3px'
cursor: isChapter ? 'pointer' : 'default',
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',
style: {
transition: 'transform 0.2s',
@ -454,10 +578,10 @@ const columns: DataTableColumns<Chapter> = [
]) : null,
h('span', {
style: {
color: row.isParent ? '#062333' : '#666666',
color: isChapter ? '#062333' : '#666666',
fontSize: '14px',
fontWeight: row.isParent ? '500' : 'normal',
marginLeft: row.isParent ? '0' : '24px'
fontWeight: isChapter ? '500' : 'normal',
marginLeft: isChapter ? '0' : '24px'
}
}, row.name)
])
@ -468,7 +592,8 @@ const columns: DataTableColumns<Chapter> = [
key: 'type',
minWidth: 60,
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('div', {
@ -489,6 +614,10 @@ const columns: DataTableColumns<Chapter> = [
key: 'sort',
minWidth: 50,
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)
}
},

194
tatus --porcelain Normal file
View File

@ -0,0 +1,194 @@
diff --git a/src/api/examples/usage.ts b/src/api/examples/usage.ts
index 0363f0d..64dbce6 100644
--- a/src/api/examples/usage.ts
+++ b/src/api/examples/usage.ts
@@ -93,13 +93,8 @@ export const searchCoursesExample = async () => {
try {
const response = await CourseApi.searchCourses({
keyword: 'Vue.js',
- category: '前端开发',
- level: 'intermediate',
- price: 'paid',
- rating: 4,
- sortBy: 'newest',
- page: 1,
- pageSize: 10
+ limit: '20',
+ page: 1
})

if (response.code === 200) {
diff --git a/src/api/modules/course.ts b/src/api/modules/course.ts
index 9d3a3e1..e8cdde2 100644
--- a/src/api/modules/course.ts
+++ b/src/api/modules/course.ts
@@ -23,7 +23,7 @@ import type {
CourseComment,
Quiz,
LearningProgress,
- SearchRequest,
+
Instructor,
} from '../types'

diff --git a/src/api/modules/exam.ts b/src/api/modules/exam.ts
index 5a1865f..f7be93a 100644
--- a/src/api/modules/exam.ts
+++ b/src/api/modules/exam.ts
@@ -333,6 +333,420 @@ export class ExamApi {
console.log('✅ 批量添加题目答案成功:', responses)
return responses
}
+
+ // ========== 试卷管理相关接口 ==========
+
+ /**
+ * 获取试卷列表
+ */
+ static async getExamPaperList(params: {
+ page?: number
+ pageSize?: number
+ keyword?: string
+ category?: string
+ status?: string
+ difficulty?: string
+ creator?: string
+ } = {}): Promise<ApiResponseWithResult<{
+ list: any[]
+ total: number
+ page: number
+ pageSize: 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 exportE