From 1721ab50fcd849421d3ab06a50b82715bba64035 Mon Sep 17 00:00:00 2001 From: QDKF Date: Sat, 13 Sep 2025 19:50:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=95=E5=8D=B7=E7=AE=A1=E7=90=86:?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AF=95=E5=8D=B7=E9=83=A8=E5=88=86=E6=8E=A5=E5=8F=A3(?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E9=A2=98=E5=BA=93=EF=BC=8C=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E9=A2=98=E7=9B=AE=EF=BC=8C=E5=AF=BC=E5=85=A5=EF=BC=8C=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=8A=9F=E8=83=BD)=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=A2=98=E5=BA=93=E7=AE=A1=E7=90=86=E8=8E=B7=E5=8F=96=E8=AF=BE?= =?UTF-8?q?=E7=A8=8B=E5=88=97=E8=A1=A8=EF=BC=8C=E5=88=86=E9=A1=B5=E5=99=A8?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=92=8C=E5=88=87=E6=8D=A2=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=AF=B9=E6=8E=A5=E5=AF=BC=E5=85=A5=E9=A2=98=E5=BA=93?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=B7=BB=E5=8A=A0=E8=AF=95=E9=A2=98?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=9F=A5=E8=AF=A2=E5=88=86=E7=B1=BB=EF=BC=8C?= =?UTF-8?q?=E9=9A=BE=E5=BA=A6=E6=8E=A5=E5=8F=A3=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=AF=95=E9=A2=98=E6=B8=B2=E6=9F=93=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=BE=91=E9=A2=98=E7=9B=AE=E6=97=B6?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E6=95=B0=E6=8D=AE=E6=97=A0=E6=B3=95=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E6=98=BE=E7=A4=BA=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=95=E9=A2=98=E7=BC=96=E8=BE=91=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ExamInfo-API-Usage.md | 523 ++++++++++++ src/api/examples/createPaper-example.ts | 112 +++ src/api/examples/deletePaper-example.ts | 180 ++++ src/api/examples/getExamInfo-example.ts | 73 ++ src/api/modules/exam.ts | 766 +++++++++++++++++- src/api/request.ts | 5 + src/api/types.ts | 40 + .../ExamComponents/ExamSettingsModal.vue | 60 +- .../ExamComponents/QuestionBankModal.vue | 617 ++++++++++++-- src/router/index.ts | 22 +- src/views/teacher/ExamPages/AddExam.vue | 529 ++++++++++-- src/views/teacher/ExamPages/AddQuestion.vue | 497 +++++++----- src/views/teacher/ExamPages/ExamLibrary.vue | 510 +++++++++--- .../ExamPages/QuestionBankManagement.vue | 105 ++- .../teacher/ExamPages/QuestionManagement.vue | 282 +++++-- 15 files changed, 3703 insertions(+), 618 deletions(-) create mode 100644 docs/ExamInfo-API-Usage.md create mode 100644 src/api/examples/createPaper-example.ts create mode 100644 src/api/examples/deletePaper-example.ts create mode 100644 src/api/examples/getExamInfo-example.ts diff --git a/docs/ExamInfo-API-Usage.md b/docs/ExamInfo-API-Usage.md new file mode 100644 index 0000000..ba9012c --- /dev/null +++ b/docs/ExamInfo-API-Usage.md @@ -0,0 +1,523 @@ +# 考试信息API使用文档 + +## 接口概述 + +`/aiol/aiolExam/getExamInfo` 接口用于获取教师名下的考试信息列表。 + +## 接口定义 + +### 请求方式 +- **方法**: GET +- **路径**: `/aiol/aiolExam/getExamInfo` +- **参数**: `userId` (查询参数) + +### 请求参数 + +| 参数名 | 类型 | 必选 | 说明 | +|--------|------|------|------| +| userId | string | 是 | 教师用户ID | + +### 响应格式 + +```typescript +{ + "success": boolean, + "message": string, + "code": number, + "result": ExamInfo[], + "timestamp": number +} +``` + +### ExamInfo 数据结构 + +```typescript +interface ExamInfo { + id: string // 考试ID + name: string // 考试名称 + paperId: string // 试卷ID + startTime: string // 开始时间 + endTime: string // 结束时间 + totalTime: number // 考试时长(分钟) + type: number // 考试类型:0=练习,1=考试 + status: number // 状态:0=未发布,1=发布中,2=已结束 + createBy: string // 创建人 + createTime: string // 创建时间 + updateBy: string // 更新人 + updateTime: string // 更新时间 +} +``` + +## 使用示例 + +### 1. 在API模块中调用 + +```typescript +import { ExamApi } from '@/api/modules/exam' + +// 获取教师考试信息 +const response = await ExamApi.getExamInfo('teacher_user_id') +console.log('考试信息:', response.data) +``` + +### 2. 在Vue组件中使用 + +```typescript +import { ExamApi } from '@/api/modules/exam' +import { useUserStore } from '@/stores/user' + +export default { + setup() { + const userStore = useUserStore() + + const loadExamInfo = async () => { + if (!userStore.user?.id) { + console.error('请先登录') + return + } + + try { + const response = await ExamApi.getExamInfo(userStore.user.id.toString()) + return response.data || [] + } catch (error) { + console.error('加载考试信息失败:', error) + return [] + } + } + + return { + loadExamInfo + } + } +} +``` + +### 3. 在试卷管理页面中使用 + +```typescript +// 在 ExamLibrary.vue 中 +import { ExamApi } from '@/api/modules/exam' +import { useUserStore } from '@/stores/user' +import type { ExamInfo } from '@/api/types' + +const userStore = useUserStore() + +const loadExamInfo = async () => { + loading.value = true + try { + const currentUser = userStore.user + if (!currentUser?.id) { + message.error('请先登录') + return + } + + const response = await ExamApi.getExamInfo(currentUser.id.toString()) + + if (response.data && Array.isArray(response.data)) { + // 数据映射和显示逻辑 + const mappedList = response.data.map((item: ExamInfo) => { + // 映射逻辑... + return { + id: item.id, + name: item.name, + // ... 其他字段映射 + } + }) + + examData.value = mappedList + } + } catch (error) { + console.error('加载考试信息失败:', error) + message.error('加载考试信息失败') + } finally { + loading.value = false + } +} +``` + +## 数据映射说明 + +### 状态映射 +```typescript +const statusMap: { [key: number]: string } = { + 0: '未发布', + 1: '发布中', + 2: '已结束' +} +``` + +### 类型映射 +```typescript +const categoryMap: { [key: number]: string } = { + 0: '练习', + 1: '考试' +} +``` + +### 难度映射 +```typescript +const difficultyMap: { [key: number]: string } = { + 0: '易', + 1: '中', + 2: '难' +} +``` + +## 错误处理 + +接口可能返回以下错误: + +1. **401 Unauthorized**: 用户未登录或token过期 +2. **403 Forbidden**: 没有权限访问 +3. **500 Internal Server Error**: 服务器内部错误 + +建议在调用时添加适当的错误处理: + +```typescript +try { + const response = await ExamApi.getExamInfo(userId) + // 处理成功响应 +} catch (error) { + if (error.response?.status === 401) { + // 处理认证错误 + console.error('登录已过期,请重新登录') + } else { + // 处理其他错误 + console.error('获取考试信息失败:', error.message) + } +} +``` + +## 注意事项 + +1. 调用此接口前需要确保用户已登录 +2. 只有教师用户才能调用此接口 +3. 返回的考试信息按创建时间倒序排列 +4. 建议在组件挂载时调用此接口加载数据 +5. 可以根据需要添加分页、搜索等参数(需要后端支持) + +## 相关文件 + +- API实现: `src/api/modules/exam.ts` +- 类型定义: `src/api/types.ts` +- 使用示例: `src/api/examples/getExamInfo-example.ts` +- 页面实现: `src/views/teacher/ExamPages/ExamLibrary.vue` + +--- + +# 创建试卷API使用文档 + +## 接口概述 + +`POST /aiol/aiolPaper/add` 接口用于创建新的试卷。 + +## 接口定义 + +### 请求方式 +- **方法**: POST +- **路径**: `/aiol/aiolPaper/add` +- **Content-Type**: `application/json` + +### 请求参数 + +| 参数名 | 类型 | 必选 | 说明 | +|--------|------|------|------| +| title | string | 是 | 试卷标题 | +| generateMode | number | 否 | 组卷模式:0=固定试卷组,1=随机抽题组卷 | +| rules | string | 否 | 组卷规则(随机抽题时使用) | +| repoId | string | 否 | 题库ID(随机抽题时使用) | +| totalScore | number | 是 | 试卷总分 | +| passScore | number | 否 | 及格分数(默认总分的60%) | +| requireReview | number | 否 | 是否需要批阅:0=不需要,1=需要 | + +### 响应格式 + +```typescript +{ + "success": boolean, + "message": string, + "code": number, + "result": string, // 试卷ID + "timestamp": number +} +``` + +## 使用示例 + +### 1. 在API模块中调用 + +```typescript +import { ExamApi } from '@/api/modules/exam' + +// 创建固定试卷组 +const response = await ExamApi.createExamPaper({ + title: '数学期末考试试卷', + generateMode: 0, + totalScore: 100, + passScore: 60, + requireReview: 0 +}) +console.log('试卷ID:', response.data) +``` + +### 2. 在Vue组件中使用 + +```typescript +// 在 AddExam.vue 中 +const saveExam = async () => { + try { + const apiData = { + title: examForm.title, + generateMode: examForm.type === 1 ? 0 : 1, + rules: '', + repoId: '', + totalScore: examForm.totalScore, + passScore: examForm.passScore || Math.floor(examForm.totalScore * 0.6), + requireReview: examForm.useAIGrading ? 1 : 0 + } + + const response = await ExamApi.createExamPaper(apiData) + console.log('创建试卷成功:', response.data) + } catch (error) { + console.error('创建试卷失败:', error) + } +} +``` + +### 3. 不同组卷模式示例 + +```typescript +// 固定试卷组 +const fixedPaper = { + title: '固定试卷组示例', + generateMode: 0, + rules: '', + repoId: '', + totalScore: 100, + passScore: 60, + requireReview: 0 +} + +// 随机抽题组卷 +const randomPaper = { + title: '随机抽题组卷示例', + generateMode: 1, + rules: '{"difficulty": [1, 2, 3], "types": [0, 1, 2], "count": 20}', + repoId: 'repo_123', + totalScore: 100, + passScore: 60, + requireReview: 1 +} +``` + +## 相关文件 + +- API实现: `src/api/modules/exam.ts` +- 页面实现: `src/views/teacher/ExamPages/AddExam.vue` +- 使用示例: `src/api/examples/createPaper-example.ts` + +--- + +# 删除试卷API使用文档 + +## 接口概述 + +删除试卷相关的API接口包括单个删除和批量删除功能。 + +## 接口定义 + +### 1. 删除单个试卷 + +- **方法**: DELETE +- **路径**: `/aiol/aiolPaper/delete?id={paperId}` +- **参数**: `id` (查询参数) - 试卷ID + +### 2. 批量删除试卷 + +- **实现方式**: 循环调用单个删除接口(因为后端可能不支持批量删除接口) +- **方法**: 内部调用多个 `DELETE /aiol/aiolPaper/delete?id={paperId}` +- **参数**: `ids: string[]` - 试卷ID数组 + +### 响应格式 + +**单个删除响应**: +```typescript +{ + "success": boolean, + "message": string, + "code": number, + "result": string, + "timestamp": number +} +``` + +**批量删除响应**: +```typescript +{ + "success": boolean, + "message": string, + "code": number, + "result": { + "success": number, // 成功删除的数量 + "failed": number, // 失败删除的数量 + "total": number, // 总数量 + "errors": string[] // 错误信息数组 + }, + "timestamp": number +} +``` + +## 使用示例 + +### 1. 删除单个试卷 + +```typescript +import { ExamApi } from '@/api/modules/exam' + +// 删除单个试卷 +const response = await ExamApi.deleteExamPaper('1962379646322384897') +console.log('删除结果:', response.data) +``` + +### 2. 批量删除试卷 + +```typescript +// 批量删除试卷 +const paperIds = ['1962379646322384897', '1966450638717292545'] +const response = await ExamApi.batchDeleteExamPapers(paperIds) +console.log('批量删除结果:', response.data) +``` + +### 3. 在Vue组件中使用(使用 Naive UI 对话框组件) + +```typescript +// 在 ExamLibrary.vue 中 +import { useDialog, useMessage } from 'naive-ui' + +const dialog = useDialog() +const message = useMessage() + +const handleDeletePaper = async (row: any) => { + try { + // 使用 Naive UI 对话框组件 + dialog.warning({ + title: '确认删除', + content: `确定要删除试卷"${row.name}"吗?此操作不可撤销。`, + positiveText: '确定删除', + negativeText: '取消', + onPositiveClick: async () => { + try { + // 调用删除API + const response = await ExamApi.deleteExamPaper(row.id) + + // 显示成功消息 + message.success('试卷删除成功!') + + // 重新加载试卷列表 + await loadExamPaperList() + } catch (error) { + console.error('删除试卷失败:', error) + message.error('删除试卷失败,请重试') + } + } + }) + } catch (error) { + console.error('删除试卷失败:', error) + message.error('删除试卷失败,请重试') + } +} + +// 批量删除示例 +const handleBatchDelete = async () => { + if (checkedRowKeys.value.length === 0) { + message.warning('请先选择要删除的试卷') + return + } + + try { + // 使用 Naive UI 对话框组件 + dialog.error({ + title: '确认批量删除', + content: `确定要删除选中的 ${checkedRowKeys.value.length} 个试卷吗?此操作不可撤销。`, + positiveText: '确定删除', + negativeText: '取消', + onPositiveClick: async () => { + try { + // 调用批量删除API + const response = await ExamApi.batchDeleteExamPapers(checkedRowKeys.value as string[]) + + // 显示成功消息 + message.success(`成功删除 ${checkedRowKeys.value.length} 个试卷!`) + + // 清空选中状态 + checkedRowKeys.value = [] + + // 重新加载试卷列表 + await loadExamPaperList() + } catch (error) { + console.error('批量删除试卷失败:', error) + message.error('批量删除试卷失败,请重试') + } + } + }) + } catch (error) { + console.error('批量删除试卷失败:', error) + message.error('批量删除试卷失败,请重试') + } +} +``` + +### 4. 错误处理 + +```typescript +const deletePaperWithErrorHandling = async (paperId: string) => { + try { + const response = await ExamApi.deleteExamPaper(paperId) + + if (response.data === 'success') { + return { success: true, message: '删除成功' } + } else { + return { success: false, message: '删除失败,请重试' } + } + + } catch (error: any) { + if (error.response?.status === 404) { + return { success: false, message: '试卷不存在' } + } else if (error.response?.status === 403) { + return { success: false, message: '没有权限删除此试卷' } + } + + return { success: false, message: '删除失败,请检查网络连接' } + } +} +``` + +## 功能特性 + +### 单个删除 +- 支持删除单个试卷 +- 使用 Naive UI 警告对话框组件 +- 删除成功后自动刷新列表 +- 完整的错误处理 + +### 批量删除 +- 支持同时删除多个试卷 +- 循环调用单个删除接口(避免后端接口不存在的问题) +- 逐个删除,避免对服务器造成过大压力 +- 详细的删除结果反馈(成功/失败数量) +- 使用 Naive UI 错误对话框组件(更醒目的警告) +- 删除后清空选中状态 + +### 用户体验 +- **美观的确认对话框**: 使用 Naive UI 组件,样式统一美观 +- **不同类型的对话框**: 单个删除使用 warning,批量删除使用 error +- **成功/失败消息提示**: 使用 Naive UI 的 message 组件 +- **按钮状态管理**: 批量删除按钮在未选中时禁用 +- **实时更新选中数量显示**: 动态显示选中的试卷数量 +- **异步操作处理**: 在对话框确认后才执行删除操作 + +## 相关文件 + +- API实现: `src/api/modules/exam.ts` +- 页面实现: `src/views/teacher/ExamPages/ExamLibrary.vue` +- 使用示例: `src/api/examples/deletePaper-example.ts` diff --git a/src/api/examples/createPaper-example.ts b/src/api/examples/createPaper-example.ts new file mode 100644 index 0000000..788047b --- /dev/null +++ b/src/api/examples/createPaper-example.ts @@ -0,0 +1,112 @@ +// 创建试卷API使用示例 +import { ExamApi } from '../modules/exam' + +/** + * 使用示例:创建试卷 + */ +export async function createPaperExample() { + try { + // 示例数据 - 匹配 /aiol/aiolPaper/add 接口 + const paperData = { + title: '数学期末考试试卷', + generateMode: 0, // 0: 固定试卷组, 1: 随机抽题组卷 + rules: '', // 组卷规则(随机抽题时使用) + repoId: '', // 题库ID(随机抽题时使用) + totalScore: 100, // 试卷总分 + passScore: 60, // 及格分数 + requireReview: 0 // 是否需要批阅:0=不需要,1=需要 + } + + console.log('🚀 准备创建试卷:', paperData) + + // 调用API创建试卷 + const response = await ExamApi.createExamPaper(paperData) + + console.log('✅ 创建试卷成功:', response) + + if (response.data) { + console.log('试卷ID:', response.data) + return response.data + } else { + console.warn('API返回的数据格式不正确') + return null + } + + } catch (error) { + console.error('创建试卷失败:', error) + throw error + } +} + +/** + * 在Vue组件中使用示例 + */ +export function useCreatePaperInComponent() { + const createPaper = async (formData: { + title: string + type: number // 1: 固定试卷组, 2: 随机抽题组卷 + totalScore: number + passScore?: number + useAIGrading?: boolean + }) => { + try { + const apiData = { + title: formData.title, + generateMode: formData.type === 1 ? 0 : 1, + rules: '', // 组卷规则 + repoId: '', // 题库ID + totalScore: formData.totalScore, + passScore: formData.passScore || Math.floor(formData.totalScore * 0.6), + requireReview: formData.useAIGrading ? 1 : 0 + } + + const response = await ExamApi.createExamPaper(apiData) + return response.data + } catch (error) { + console.error('创建试卷失败:', error) + throw error + } + } + + return { + createPaper + } +} + +/** + * 不同组卷模式的示例 + */ +export const paperCreationExamples = { + // 固定试卷组示例 + fixedPaper: { + title: '固定试卷组示例', + generateMode: 0, + rules: '', + repoId: '', + totalScore: 100, + passScore: 60, + requireReview: 0 + }, + + // 随机抽题组卷示例 + randomPaper: { + title: '随机抽题组卷示例', + generateMode: 1, + rules: '{"difficulty": [1, 2, 3], "types": [0, 1, 2], "count": 20}', + repoId: 'repo_123', + totalScore: 100, + passScore: 60, + requireReview: 1 + }, + + // 需要AI批阅的试卷示例 + aiGradingPaper: { + title: 'AI批阅试卷示例', + generateMode: 0, + rules: '', + repoId: '', + totalScore: 100, + passScore: 60, + requireReview: 1 + } +} diff --git a/src/api/examples/deletePaper-example.ts b/src/api/examples/deletePaper-example.ts new file mode 100644 index 0000000..81e629b --- /dev/null +++ b/src/api/examples/deletePaper-example.ts @@ -0,0 +1,180 @@ +// 删除试卷API使用示例 +import { ExamApi } from '../modules/exam' + +/** + * 使用示例:删除单个试卷 + */ +export async function deletePaperExample() { + try { + const paperId = '1962379646322384897' // 试卷ID + + console.log('🚀 准备删除试卷:', paperId) + + // 调用删除API + const response = await ExamApi.deleteExamPaper(paperId) + + console.log('✅ 删除试卷成功:', response) + + if (response.data) { + console.log('删除结果:', response.data) + return true + } else { + console.warn('删除操作可能失败') + return false + } + + } catch (error) { + console.error('删除试卷失败:', error) + throw error + } +} + +/** + * 使用示例:批量删除试卷 + */ +export async function batchDeletePapersExample() { + try { + const paperIds = [ + '1962379646322384897', + '1966450638717292545', + '1966458655621877761' + ] // 试卷ID数组 + + console.log('🚀 准备批量删除试卷:', paperIds) + + // 调用批量删除API + const response = await ExamApi.batchDeleteExamPapers(paperIds) + + console.log('✅ 批量删除试卷完成:', response) + + if (response.data) { + const { success, failed, total, errors } = response.data + console.log('批量删除结果:', { + 总数: total, + 成功: success, + 失败: failed, + 错误: errors + }) + + if (failed === 0) { + console.log('✅ 所有试卷删除成功') + return { success: true, message: `成功删除 ${success} 个试卷` } + } else if (success > 0) { + console.warn('⚠️ 部分试卷删除成功') + return { success: false, message: `删除完成:成功 ${success} 个,失败 ${failed} 个` } + } else { + console.error('❌ 所有试卷删除失败') + return { success: false, message: `删除失败:${failed} 个试卷删除失败` } + } + } else { + console.warn('批量删除操作可能失败') + return { success: false, message: '批量删除操作失败' } + } + + } catch (error) { + console.error('批量删除试卷失败:', error) + throw error + } +} + +/** + * 在Vue组件中使用示例(使用 Naive UI 对话框组件) + */ +export function useDeletePaperInComponent() { + // 注意:在实际使用中需要从 'naive-ui' 导入 useDialog 和 useMessage + // import { useDialog, useMessage } from 'naive-ui' + + const deletePaper = async (paperId: string, paperName: string, dialog: any, message: any) => { + try { + // 使用 Naive UI 对话框组件 + dialog.warning({ + title: '确认删除', + content: `确定要删除试卷"${paperName}"吗?此操作不可撤销。`, + positiveText: '确定删除', + negativeText: '取消', + onPositiveClick: async () => { + try { + const response = await ExamApi.deleteExamPaper(paperId) + console.log('删除试卷成功:', response) + message.success('试卷删除成功!') + return true + } catch (error) { + console.error('删除试卷失败:', error) + message.error('删除试卷失败,请重试') + throw error + } + } + }) + } catch (error) { + console.error('删除试卷失败:', error) + throw error + } + } + + const batchDeletePapers = async (paperIds: string[], dialog: any, message: any) => { + try { + // 使用 Naive UI 对话框组件 + dialog.error({ + title: '确认批量删除', + content: `确定要删除选中的 ${paperIds.length} 个试卷吗?此操作不可撤销。`, + positiveText: '确定删除', + negativeText: '取消', + onPositiveClick: async () => { + try { + const response = await ExamApi.batchDeleteExamPapers(paperIds) + console.log('批量删除试卷成功:', response) + message.success(`成功删除 ${paperIds.length} 个试卷!`) + return true + } catch (error) { + console.error('批量删除试卷失败:', error) + message.error('批量删除试卷失败,请重试') + throw error + } + } + }) + } catch (error) { + console.error('批量删除试卷失败:', error) + throw error + } + } + + return { + deletePaper, + batchDeletePapers + } +} + +/** + * 错误处理示例 + */ +export async function deletePaperWithErrorHandling(paperId: string) { + try { + const response = await ExamApi.deleteExamPaper(paperId) + + // 检查响应状态 + if (response.data && response.data === 'success') { + console.log('试卷删除成功') + return { success: true, message: '删除成功' } + } else { + console.warn('删除操作可能失败:', response) + return { success: false, message: '删除失败,请重试' } + } + + } catch (error: any) { + console.error('删除试卷时发生错误:', error) + + // 根据错误类型返回不同的错误信息 + if (error.response) { + const status = error.response.status + if (status === 404) { + return { success: false, message: '试卷不存在' } + } else if (status === 403) { + return { success: false, message: '没有权限删除此试卷' } + } else if (status === 500) { + return { success: false, message: '服务器错误,请稍后重试' } + } + } + + return { success: false, message: '删除失败,请检查网络连接' } + } +} diff --git a/src/api/examples/getExamInfo-example.ts b/src/api/examples/getExamInfo-example.ts new file mode 100644 index 0000000..51aaeb9 --- /dev/null +++ b/src/api/examples/getExamInfo-example.ts @@ -0,0 +1,73 @@ +// getExamInfo 接口使用示例 +import { ExamApi } from '../modules/exam' +import { useUserStore } from '@/stores/user' + +/** + * 使用示例:获取教师名下的考试信息 + */ +export async function getExamInfoExample() { + try { + // 获取用户存储 + const userStore = useUserStore() + + // 确保用户已登录 + if (!userStore.user || !userStore.user.id) { + console.error('用户未登录') + return + } + + // 调用API获取考试信息 + const response = await ExamApi.getExamInfo(userStore.user.id.toString()) + + console.log('API响应:', response) + + if (response.data && Array.isArray(response.data)) { + console.log('考试信息列表:', response.data) + + // 处理每个考试信息 + response.data.forEach((exam, index) => { + console.log(`考试 ${index + 1}:`, { + id: exam.id, + name: exam.name, + type: exam.type, + status: exam.status, + startTime: exam.startTime, + endTime: exam.endTime, + createBy: exam.createBy, + createTime: exam.createTime + }) + }) + } else { + console.warn('API返回的数据格式不正确') + } + + } catch (error) { + console.error('获取考试信息失败:', error) + } +} + +/** + * 在Vue组件中使用示例 + */ +export function useExamInfoInComponent() { + const userStore = useUserStore() + + const loadExamInfo = async () => { + if (!userStore.user?.id) { + console.error('请先登录') + return [] + } + + try { + const response = await ExamApi.getExamInfo(userStore.user.id.toString()) + return response.data || [] + } catch (error) { + console.error('加载考试信息失败:', error) + return [] + } + } + + return { + loadExamInfo + } +} diff --git a/src/api/modules/exam.ts b/src/api/modules/exam.ts index 9f7a132..cb1bc0f 100644 --- a/src/api/modules/exam.ts +++ b/src/api/modules/exam.ts @@ -1,5 +1,6 @@ // 考试题库相关API接口 import { ApiRequest } from '../request' +import axios from 'axios' import type { ApiResponse, ApiResponseWithResult, @@ -15,6 +16,7 @@ import type { UpdateQuestionAnswerRequest, CreateQuestionRepoRequest, UpdateQuestionRepoRequest, + ExamInfo, } from '../types' /** @@ -48,8 +50,8 @@ export class ExamApi { */ static async getCourseList(): Promise> { try { - // 调用现有的课程列表API,但只返回id和name字段 - const response = await ApiRequest.get('/biz/course/list') + // 调用教师端课程列表API + const response = await ApiRequest.get('/aiol/aiolCourse/teacher_list') console.log('✅ 获取课程列表成功:', response) // 处理响应数据,只提取id和name @@ -164,9 +166,9 @@ export class ExamApi { /** * 查询题库下题目 */ - static async getQuestionsByRepo(repoId: string): Promise> { + static async getQuestionsByRepo(repoId: string): Promise> { console.log('🚀 查询题库下题目:', { repoId }) - const response = await ApiRequest.get(`/aiol/aiolRepo/questionList/${repoId}`) + const response = await ApiRequest.get<{ result: Question[] }>(`/aiol/aiolRepo/questionList/${repoId}`) console.log('✅ 查询题库下题目成功:', response) return response } @@ -176,7 +178,7 @@ export class ExamApi { */ static async getQuestionDetail(questionId: string): Promise> { console.log('🚀 查询题目详情:', { questionId }) - const response = await ApiRequest.get(`/aiol/aiolRepo/repoList/${questionId}`) + const response = await ApiRequest.get(`/aiol/aiolQuestion/queryById?id=${questionId}`) console.log('✅ 查询题目详情成功:', response) return response } @@ -253,13 +255,23 @@ export class ExamApi { */ static async deleteQuestionOption(id: string): Promise> { console.log('🚀 删除题目选项:', { id }) - const response = await ApiRequest.delete('/gen/questionoption/questionOption/delete', { + const response = await ApiRequest.delete('/aiol/aiolQuestionOption/delete', { params: { id } }) console.log('✅ 删除题目选项成功:', response) return response } + /** + * 获取题目选项列表 + */ + static async getQuestionOptions(questionId: string): Promise> { + console.log('🚀 获取题目选项列表:', { questionId }) + const response = await ApiRequest.get(`/aiol/aiolQuestionOption/list?questionId=${questionId}`) + console.log('✅ 获取题目选项列表成功:', response) + return response + } + // ========== 题目答案管理 ========== /** @@ -287,7 +299,7 @@ export class ExamApi { */ static async deleteQuestionAnswer(id: string): Promise> { console.log('🚀 删除题目答案:', { id }) - const response = await ApiRequest.delete('/gen/questionanswer/questionAnswer/delete', { + const response = await ApiRequest.delete('/aiol/aiolQuestionAnswer/delete', { params: { id } }) console.log('✅ 删除题目答案成功:', response) @@ -301,7 +313,7 @@ export class ExamApi { */ static async createQuestionRepo(data: CreateQuestionRepoRequest): Promise> { console.log('🚀 添加题库题目关联:', data) - const response = await ApiRequest.post('/gen/questionrepo/questionRepo/add', data) + const response = await ApiRequest.post('/aiol/aiolQuestionRepo/add', data) console.log('✅ 添加题库题目关联成功:', response) return response } @@ -311,7 +323,7 @@ export class ExamApi { */ static async updateQuestionRepo(data: UpdateQuestionRepoRequest): Promise> { console.log('🚀 编辑题库题目关联:', data) - const response = await ApiRequest.put('/gen/questionrepo/questionRepo/edit', data) + const response = await ApiRequest.put('/aiol/aiolQuestionRepo/edit', data) console.log('✅ 编辑题库题目关联成功:', response) return response } @@ -321,13 +333,370 @@ export class ExamApi { */ static async deleteQuestionRepo(id: string): Promise> { console.log('🚀 删除题库题目关联:', { id }) - const response = await ApiRequest.delete('/gen/questionrepo/questionRepo/delete', { + const response = await ApiRequest.delete('/aiol/aiolQuestionRepo/delete', { params: { id } }) console.log('✅ 删除题库题目关联成功:', response) return response } + // ========== 题目完整创建流程 ========== + + /** + * 创建完整题目(包含选项、答案和题库关联) + */ + static async createCompleteQuestion(data: { + repoId: string + parentId?: string + type: number + content: string + analysis?: string + difficulty: number + score: number + degree?: number + ability?: number + options?: Array<{ + content: string + isCorrect: boolean + orderNo: number + }> + answers?: Array<{ + answerText: string + orderNo: number + }> + }): Promise> { + try { + console.log('🚀 开始创建完整题目:', data) + + // 1. 创建题目 + const questionData = { + repoId: data.repoId, + parentId: data.parentId, + type: data.type, + content: data.content, + analysis: data.analysis || '', + difficulty: data.difficulty, + score: data.score, + degree: data.degree || 1, + ability: data.ability || 1 + } + + console.log('📝 步骤1: 创建题目基础信息') + const questionResponse = await this.createQuestion(questionData) + + if (!questionResponse.data) { + throw new Error('创建题目失败:未获取到题目ID') + } + + const questionId = questionResponse.data + console.log('✅ 题目创建成功,ID:', questionId) + + // 确保questionId是字符串 + let questionIdStr = ''; + if (typeof questionId === 'string') { + questionIdStr = questionId; + } else if (questionId && typeof questionId === 'object' && (questionId as any).result) { + questionIdStr = String((questionId as any).result); + } else { + questionIdStr = String(questionId); + } + + console.log('🔍 处理后的题目ID:', questionIdStr); + + // 2. 创建题目选项(选择题需要) + if (data.options && data.options.length > 0) { + console.log('📝 步骤2: 创建题目选项') + for (const option of data.options) { + await this.createQuestionOption({ + questionId: questionIdStr, // 使用处理后的题目ID + content: option.content, + izCorrent: option.isCorrect ? 1 : 0, + orderNo: option.orderNo + }) + } + console.log('✅ 题目选项创建成功') + } + + // 3. 创建题目答案(填空题、简答题需要) + if (data.answers && data.answers.length > 0) { + console.log('📝 步骤3: 创建题目答案') + for (const answer of data.answers) { + await this.createQuestionAnswer({ + questionId: questionIdStr, // 使用处理后的题目ID + answerText: answer.answerText, + orderNo: answer.orderNo + }) + } + console.log('✅ 题目答案创建成功') + } + + // 4. 创建题库题目关联 + console.log('📝 步骤4: 创建题库题目关联') + await this.createQuestionRepo({ + repoId: data.repoId, + questionId: questionIdStr // 使用处理后的题目ID + }) + console.log('✅ 题库题目关联创建成功') + + console.log('🎉 完整题目创建成功!') + return { + code: 200, + message: '题目创建成功', + data: questionId + } + + } catch (error: any) { + console.error('❌ 创建完整题目失败:', error) + throw error + } + } + + // ========== 题目完整编辑流程 ========== + + /** + * 编辑完整题目(包含选项、答案和题库关联) + */ + static async updateCompleteQuestion(data: { + questionId: string + repoId: string + parentId?: string + type: number + content: string + analysis?: string + difficulty: number + score: number + degree?: number + ability?: number + options?: Array<{ + id?: string + content: string + isCorrect: boolean + orderNo: number + }> + answers?: Array<{ + id?: string + answerText: string + orderNo: number + }> + }): Promise> { + try { + console.log('🚀 开始编辑完整题目:', data) + + // 1. 更新题目基础信息 + const questionData = { + id: data.questionId, + parentId: data.parentId, + type: data.type, + content: data.content, + analysis: data.analysis || '', + difficulty: data.difficulty, + score: data.score + } + + console.log('📝 步骤1: 更新题目基础信息') + await this.updateQuestion(questionData) + console.log('✅ 题目基础信息更新成功') + + // 2. 处理题目选项(选择题需要) + if (data.options && data.options.length > 0) { + console.log('📝 步骤2: 处理题目选项') + + // 先获取现有选项 + const existingOptions = await this.getQuestionOptions(data.questionId) + let existingOptionsList = [] + + // 处理不同格式的API响应 + if (existingOptions.data) { + const data = existingOptions.data as any + if (Array.isArray(data)) { + existingOptionsList = data + } else if (data.result && Array.isArray(data.result)) { + existingOptionsList = data.result + } else if (data.result && data.result.records && Array.isArray(data.result.records)) { + existingOptionsList = data.result.records + } + } + + console.log('🔍 现有选项:', existingOptionsList) + + // 删除所有现有选项 + const deletePromises = existingOptionsList.map(async (existingOption: any) => { + try { + await this.deleteQuestionOption(existingOption.id) + console.log(`🗑️ 删除现有选项成功: ${existingOption.id}`) + return true + } catch (error) { + console.warn(`删除选项失败: ${existingOption.id}`, error) + return false + } + }) + + // 等待所有删除操作完成 + const deleteResults = await Promise.allSettled(deletePromises) + const successCount = deleteResults.filter(result => result.status === 'fulfilled').length + console.log(`🗑️ 删除选项结果: ${successCount}/${existingOptionsList.length} 成功`) + + // 等待一小段时间确保删除操作完成 + await new Promise(resolve => setTimeout(resolve, 200)) + + // 创建新选项 + for (let i = 0; i < data.options.length; i++) { + const option = data.options[i] + await this.createQuestionOption({ + questionId: data.questionId, + content: option.content, + izCorrent: option.isCorrect ? 1 : 0, + orderNo: i // 确保orderNo从0开始连续 + }) + } + console.log('✅ 题目选项更新成功') + } + + // 3. 处理题目答案(填空题、简答题需要) + if (data.answers && data.answers.length > 0) { + console.log('📝 步骤3: 处理题目答案') + + // 先获取现有答案 + const existingAnswers = await this.getQuestionAnswers(data.questionId) + let existingAnswersList = [] + + // 处理不同格式的API响应 + if (existingAnswers.data) { + const data = existingAnswers.data as any + if (Array.isArray(data)) { + existingAnswersList = data + } else if (data.result && Array.isArray(data.result)) { + existingAnswersList = data.result + } else if (data.result && data.result.records && Array.isArray(data.result.records)) { + existingAnswersList = data.result.records + } + } + + console.log('🔍 现有答案:', existingAnswersList) + + // 删除所有现有答案 + const deleteAnswerPromises = existingAnswersList.map(async (existingAnswer: any) => { + try { + await this.deleteQuestionAnswer(existingAnswer.id) + console.log(`🗑️ 删除现有答案成功: ${existingAnswer.id}`) + return true + } catch (error) { + console.warn(`删除答案失败: ${existingAnswer.id}`, error) + return false + } + }) + + // 等待所有删除操作完成 + const deleteAnswerResults = await Promise.allSettled(deleteAnswerPromises) + const successAnswerCount = deleteAnswerResults.filter(result => result.status === 'fulfilled').length + console.log(`🗑️ 删除答案结果: ${successAnswerCount}/${existingAnswersList.length} 成功`) + + // 等待一小段时间确保删除操作完成 + await new Promise(resolve => setTimeout(resolve, 200)) + + // 创建新答案 + for (let i = 0; i < data.answers.length; i++) { + const answer = data.answers[i] + await this.createQuestionAnswer({ + questionId: data.questionId, + answerText: answer.answerText, + orderNo: i // 确保orderNo从0开始连续 + }) + } + console.log('✅ 题目答案更新成功') + } + + console.log('🎉 完整题目编辑成功!') + return { + code: 200, + message: '题目编辑成功', + data: data.questionId + } + + } catch (error: any) { + console.error('❌ 编辑完整题目失败:', error) + throw error + } + } + + /** + * 获取题目答案列表 + */ + static async getQuestionAnswers(questionId: string): Promise> { + console.log('🚀 获取题目答案列表:', { questionId }) + const response = await ApiRequest.get(`/aiol/aiolQuestionAnswer/list?questionId=${questionId}`) + console.log('✅ 获取题目答案列表成功:', response) + return response + } + + // ========== 分类和难度管理 ========== + + /** + * 获取题目分类列表 + */ + static async getQuestionCategories(): Promise>> { + try { + console.log('🚀 获取题目分类列表') + const response = await ApiRequest.get('/aiol/aiolCourse/category/list') + console.log('✅ 获取题目分类列表成功:', response) + + if (response.data && response.data.success && response.data.result) { + const categories = response.data.result.map((item: any) => ({ + id: String(item.id), + name: item.name || '未命名分类' + })) + + return { + code: 200, + message: '获取成功', + data: categories + } + } + + return { + code: 200, + message: '获取成功', + data: [] + } + } catch (error) { + console.error('❌ 获取题目分类失败:', error) + throw error + } + } + + /** + * 获取题目难度列表 + */ + static async getQuestionDifficulties(): Promise>> { + try { + console.log('🚀 获取题目难度列表') + const response = await ApiRequest.get('/aiol/aiolCourse/difficulty/list') + console.log('✅ 获取题目难度列表成功:', response) + + if (response.data && response.data.success && response.data.result) { + const difficulties = response.data.result.map((item: any) => ({ + id: String(item.value || '0'), + name: item.label || '未知难度' + })) + + return { + code: 200, + message: '获取成功', + data: difficulties + } + } + + return { + code: 200, + message: '获取成功', + data: [] + } + } catch (error) { + console.error('❌ 获取题目难度失败:', error) + throw error + } + } + // ========== 常用工具方法 ========== /** @@ -357,6 +726,166 @@ export class ExamApi { return difficultyMap[difficulty] || '未知难度' } + /** + * 导入题目Excel文件 + */ + static async importQuestions(repoId: string, formData: FormData): Promise> { + console.log('🚀 导入题目Excel文件:', { repoId }) + + try { + const response = await ApiRequest.post(`/aiol/aiolRepo/importXls?repoId=${repoId}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + console.log('✅ 导入题目Excel文件成功:', response) + return response + } catch (error: any) { + console.error('❌ 导入题目Excel文件失败:', error) + + // 处理服务器返回的错误信息 + if (error.response && error.response.data) { + const errorData = error.response.data + if (errorData.message) { + throw new Error(errorData.message) + } + } + + // 处理网络错误 + if (error.message) { + throw new Error(error.message) + } + + throw new Error('导入失败,请检查网络连接') + } + } + + /** + * 下载题库导入模板 + */ + static async downloadTemplate(): Promise { + try { + console.log('🚀 下载题库导入模板') + + // 直接使用axios请求,避免响应拦截器的干扰 + const baseURL = import.meta.env.VITE_API_BASE_URL || '/jeecgboot' + const token = localStorage.getItem('X-Access-Token') || '' + + const response = await axios.get(`${baseURL}/aiol/aiolRepo/exportXls?repoId=template`, { + responseType: 'blob', + headers: { + 'X-Access-Token': token, + 'X-Request-Time': Date.now().toString() + } + }) + + console.log('✅ 下载题库导入模板成功:', response) + + // 检查响应数据 + if (response.data instanceof Blob) { + return response.data + } + + // 如果数据是ArrayBuffer,转换为Blob + if (response.data instanceof ArrayBuffer) { + return new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + } + + // 如果数据是字符串,尝试base64解码 + if (typeof response.data === 'string') { + try { + const binaryString = atob(response.data) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + } catch (error) { + console.error('base64解码失败:', error) + throw new Error('文件数据格式不正确') + } + } + + console.error('无法解析的数据类型:', { + type: typeof response.data, + constructor: response.data?.constructor?.name, + data: response.data + }) + throw new Error('无法解析服务器返回的文件数据') + } catch (error: any) { + console.error('下载题库导入模板失败:', error) + throw error + } + } + + /** + * 导出题目Excel文件 + */ + static async exportQuestions(repoId: string): Promise { + console.log('🚀 导出题目Excel文件:', { repoId }) + + // 直接使用axios请求,避免响应拦截器的干扰 + const baseURL = import.meta.env.VITE_API_BASE_URL || '/jeecgboot' + const token = localStorage.getItem('X-Access-Token') || '' + + const response = await axios.get(`${baseURL}/aiol/aiolRepo/exportXls?repoId=${repoId}`, { + responseType: 'blob', + headers: { + 'X-Access-Token': token, + 'X-Request-Time': Date.now().toString() + } + }) + + console.log('✅ 导出题目Excel文件成功:', response) + + // 检查响应数据 + if (response.data instanceof Blob) { + return response.data + } + + // 如果数据是ArrayBuffer,转换为Blob + if (response.data instanceof ArrayBuffer) { + return new Blob([response.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + } + + // 如果数据是字符串,尝试base64解码 + if (typeof response.data === 'string') { + try { + const binaryString = atob(response.data) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + } catch (error) { + console.error('base64解码失败:', error) + throw new Error('文件数据格式不正确') + } + } + + console.error('无法解析的数据类型:', { + type: typeof response.data, + constructor: response.data?.constructor?.name, + data: response.data + }) + throw new Error('无法解析服务器返回的文件数据') + } catch (error: any) { + console.error('导出题目Excel文件失败:', error) + if (error.response) { + // 服务器返回了错误响应 + console.error('服务器错误:', error.response.status, error.response.data) + throw new Error(`服务器错误: ${error.response.status} - ${error.response.data?.message || '未知错误'}`) + } else if (error.request) { + // 请求已发出但没有收到响应 + console.error('网络错误:', error.request) + throw new Error('网络连接失败,请检查网络连接') + } else { + // 其他错误 + console.error('请求配置错误:', error.message) + throw new Error(`请求失败: ${error.message}`) + } + } + /** * 批量添加题目选项 */ @@ -395,6 +924,16 @@ export class ExamApi { // ========== 试卷管理相关接口 ========== + /** + * 获取教师名下的考试信息 + */ + static async getExamInfo(userId: string): Promise> { + console.log('🚀 获取教师名下的考试信息:', { userId }) + const response = await ApiRequest.get(`/aiol/aiolExam/getExamInfo?userId=${userId}`) + console.log('✅ 获取教师名下的考试信息成功:', response) + return response + } + /** * 获取试卷列表 */ @@ -430,27 +969,132 @@ export class ExamApi { /** * 获取试卷详情 */ - static async getExamPaperDetail(id: string): Promise> { + static async getExamPaperDetail(id: string): Promise> { console.log('🚀 获取试卷详情:', id) - const response = await ApiRequest.get(`/aiol/aiolExam/paperDetail/${id}`) + const response = await ApiRequest.get<{ + success: boolean + message: string + result: { + id: string + title: string + generateMode: number + rules: string + repoId: string + totalScore: number + passScore: number + requireReview: number + createBy: string + createTime: string + updateBy: string + updateTime: string + } + }>(`/aiol/aiolPaper/queryById`, { id }) console.log('✅ 获取试卷详情成功:', response) return response } + /** + * 获取试卷题目列表 + */ + static async getExamPaperQuestions(paperId: string): Promise> { + console.log('🚀 获取试卷题目列表:', paperId) + const response = await ApiRequest.get<{ + success: boolean + message: string + result: any[] + }>(`/aiol/aiolPaperQuestion/list`, { paperId }) + console.log('✅ 获取试卷题目列表成功:', response) + return response + } + + /** + * 添加试卷题目 + */ + static async addExamPaperQuestion(data: { + paperId: string + questionId: string + orderNo: number + score: number + }): Promise> { + console.log('🚀 添加试卷题目:', data) + const response = await ApiRequest.post(`/aiol/aiolPaperQuestion/add`, data) + console.log('✅ 添加试卷题目成功:', response) + return response + } + + /** + * 编辑试卷题目 + */ + static async updateExamPaperQuestion(data: { + id: string + paperId: string + questionId: string + orderNo: number + score: number + }): Promise> { + console.log('🚀 编辑试卷题目:', data) + const response = await ApiRequest.put(`/aiol/aiolPaperQuestion/edit`, data) + console.log('✅ 编辑试卷题目成功:', response) + return response + } + + /** + * 删除试卷题目 + */ + static async deleteExamPaperQuestion(id: string): Promise> { + console.log('🚀 删除试卷题目:', id) + const response = await ApiRequest.delete(`/aiol/aiolPaperQuestion/delete`, { id }) + console.log('✅ 删除试卷题目成功:', response) + return response + } + /** * 创建试卷 */ static async createExamPaper(data: { - name: string - category: string - description?: string + title: string + generateMode?: number + rules?: string + repoId?: string totalScore: number - difficulty: string - duration: number - questions: any[] + passScore?: number + requireReview?: number }): Promise> { console.log('🚀 创建试卷:', data) - const response = await ApiRequest.post('/aiol/aiolPaper/add', data) + + // 构建API请求数据,确保字段名匹配后端接口 + const apiData = { + title: data.title, + generateMode: data.generateMode || 0, // 默认固定试卷组 + rules: data.rules || '', // 组卷规则 + repoId: data.repoId || '', // 题库ID + totalScore: data.totalScore, + passScore: data.passScore || Math.floor(data.totalScore * 0.6), // 默认及格分为总分的60% + requireReview: data.requireReview || 0 // 默认不需要批阅 + } + + const response = await ApiRequest.post('/aiol/aiolPaper/add', apiData) console.log('✅ 创建试卷成功:', response) return response } @@ -459,16 +1103,33 @@ export class ExamApi { * 更新试卷 */ static async updateExamPaper(id: string, data: { - name?: string - category?: string + title: string + generateMode?: number + rules?: string + repoId?: string + totalScore: number + passScore?: number + requireReview?: number description?: string - totalScore?: number - difficulty?: string duration?: number - questions?: any[] + instructions?: string + useAIGrading?: boolean }): Promise> { console.log('🚀 更新试卷:', { id, data }) - const response = await ApiRequest.put(`/aiol/aiolExam/paperUpdate/${id}`, data) + + // 准备API数据 - 匹配后端接口格式 + const apiData = { + id: id, + title: data.title, + generateMode: data.generateMode || 0, // 0: 固定试卷组, 1: 随机抽题组卷 + rules: data.rules || '', // 组卷规则 + repoId: data.repoId || '', // 题库ID + totalScore: data.totalScore, + passScore: data.passScore || Math.floor(data.totalScore * 0.6), // 及格分 + requireReview: data.requireReview || (data.useAIGrading ? 1 : 0) // 是否需要批阅 + } + + const response = await ApiRequest.put(`/aiol/aiolPaper/edit`, apiData) console.log('✅ 更新试卷成功:', response) return response } @@ -478,19 +1139,64 @@ export class ExamApi { */ static async deleteExamPaper(id: string): Promise> { console.log('🚀 删除试卷:', id) - const response = await ApiRequest.delete(`/aiol/aiolExam/paperDelete/${id}`) + const response = await ApiRequest.delete(`/aiol/aiolPaper/delete?id=${id}`) console.log('✅ 删除试卷成功:', response) return response } /** * 批量删除试卷 + * 由于后端可能不支持批量删除接口,改为循环调用单个删除接口 */ - static async batchDeleteExamPapers(ids: string[]): Promise> { + static async batchDeleteExamPapers(ids: string[]): Promise> { console.log('🚀 批量删除试卷:', ids) - const response = await ApiRequest.post('/aiol/aiolExam/paperBatchDelete', { ids }) - console.log('✅ 批量删除试卷成功:', response) - return response + + const results = { + success: 0, + failed: 0, + total: ids.length, + errors: [] as string[] + } + + try { + // 逐个删除,而不是并行删除,避免对服务器造成过大压力 + for (let i = 0; i < ids.length; i++) { + const id = ids[i] + try { + console.log(`🗑️ 正在删除第 ${i + 1}/${ids.length} 个试卷:`, id) + await this.deleteExamPaper(id) + results.success++ + console.log(`✅ 第 ${i + 1} 个试卷删除成功`) + } catch (error) { + results.failed++ + const errorMsg = `试卷 ${id} 删除失败: ${error}` + results.errors.push(errorMsg) + console.error(`❌ 第 ${i + 1} 个试卷删除失败:`, error) + } + } + + console.log('✅ 批量删除完成:', results) + + // 返回结果 + return { + data: results, + success: results.failed === 0, + message: results.failed === 0 + ? `成功删除 ${results.success} 个试卷` + : `删除完成,成功 ${results.success} 个,失败 ${results.failed} 个`, + code: results.failed === 0 ? 200 : 207, // 207 表示部分成功 + timestamp: Date.now().toString() + } as ApiResponse + + } catch (error) { + console.error('❌ 批量删除试卷失败:', error) + throw error + } } /** diff --git a/src/api/request.ts b/src/api/request.ts index 04cafef..c116751 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -94,6 +94,11 @@ request.interceptors.response.use( }) } + // 如果是blob响应,直接返回 + if (response.config.responseType === 'blob') { + return response + } + // 处理不同的响应格式 let normalizedData: ApiResponse diff --git a/src/api/types.ts b/src/api/types.ts index ca85f3f..3a3926b 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -850,3 +850,43 @@ export interface UpdateQuestionRepoRequest { repoId: string questionId: string } + +// 考试信息类型 +export interface ExamInfo { + id: string + name: string + paperId: string + startTime: string + endTime: string + totalTime: number + type: number + status: number + createBy: string + createTime: string + updateBy: string + updateTime: string + // 扩展字段,用于试卷管理页面显示 + category?: string + questionCount?: number + chapter?: string + totalScore?: number + difficulty?: string + creator?: string + creationTime?: string +} + +// 试卷信息类型 +export interface PaperInfo { + id: string + title: string + generateMode: number + rules: string + repoId: string + totalScore: number + passScore: number + requireReview: number + createBy: string + createTime: string + updateBy: string + updateTime: string +} diff --git a/src/components/admin/ExamComponents/ExamSettingsModal.vue b/src/components/admin/ExamComponents/ExamSettingsModal.vue index a96c790..564ad66 100644 --- a/src/components/admin/ExamComponents/ExamSettingsModal.vue +++ b/src/components/admin/ExamComponents/ExamSettingsModal.vue @@ -24,7 +24,8 @@
- +
@@ -206,9 +207,12 @@
- 填空题、简答题题目设为为主观题 设为主观题后需教师手动批阅 - 填空题、简答题不区分大小写 勾选后,英文大写和小写都可以得分 - 填空题、简答题忽略符号 勾选后,答案内符号与标准答案不同也给分 + 填空题、简答题题目设为为主观题 设为主观题后需教师手动批阅 + + 填空题、简答题不区分大小写 勾选后,英文大写和小写都可以得分 + + 填空题、简答题忽略符号 勾选后,答案内符号与标准答案不同也给分 + 多选题未全选对时得一半分 不勾选时全选对才给分
@@ -274,7 +278,19 @@ @@ -334,7 +755,7 @@ onMounted(() => { position: relative; } -.header-title{ +.header-title { color: #000; font-weight: 400; font-size: 20px; @@ -379,7 +800,7 @@ onMounted(() => { margin-left: auto; } -.tip{ +.tip { font-size: 12px; color: #999; } @@ -439,6 +860,24 @@ onMounted(() => { margin-bottom: 16px; } +/* 空状态样式 */ +.empty-state { + padding: 40px 20px; + text-align: center; +} + +.empty-tip { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + color: #999; +} + +.empty-tip p { + margin: 0; + font-size: 14px; +} /* 响应式设计 */ @media (max-width: 1024px) { .question-bank-content { diff --git a/src/router/index.ts b/src/router/index.ts index 04b1bd3..67932fa 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -86,6 +86,7 @@ import StudentList from '@/views/teacher/ExamPages/StudentList.vue' import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue' import ExamTaking from '@/views/teacher/ExamPages/ExamTaking.vue' import ExamNoticeBeforeStart from '@/views/teacher/ExamPages/ExamNoticeBeforeStart.vue' +import ExamAnalysis from '@/views/teacher/ExamPages/ExamAnalysis.vue' import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue' import TeacherCourseDetail from '@/views/teacher/course/CourseDetail.vue' @@ -414,7 +415,14 @@ const routes: RouteRecordRaw[] = [ name: 'ExamManagement', component: ExamManagement, meta: { title: '考试管理' }, - redirect: '/teacher/exam-management/question-bank', + redirect: (to) => { + // 如果访问的是根路径,重定向到题库管理 + if (to.path === '/teacher/exam-management') { + return '/teacher/exam-management/question-bank' + } + // 否则不重定向,让子路由处理 + return '/teacher/exam-management/question-bank' + }, children: [ { path: 'question-bank', @@ -467,6 +475,12 @@ const routes: RouteRecordRaw[] = [ component: AddExam, meta: { title: '添加试卷' } }, + { + path: 'edit/:id', + name: 'EditExam', + component: AddExam, + meta: { title: '编辑试卷' } + }, { path: 'preview', name: 'ExamPreview', @@ -478,6 +492,12 @@ const routes: RouteRecordRaw[] = [ name: 'AddQuestionPage', component: AddQuestion, meta: { title: '添加试题' } + }, + { + path: 'analysis', + name: 'ExamAnalysis', + component: ExamAnalysis, + meta: { title: '试卷分析' } } ] }, diff --git a/src/views/teacher/ExamPages/AddExam.vue b/src/views/teacher/ExamPages/AddExam.vue index c23a4f9..457449e 100644 --- a/src/views/teacher/ExamPages/AddExam.vue +++ b/src/views/teacher/ExamPages/AddExam.vue @@ -10,7 +10,7 @@ -

添加试卷

+

{{ isEditMode ? '编辑试卷' : '添加试卷' }}

@@ -182,7 +182,7 @@
@@ -249,6 +249,17 @@