feat: 试卷管理:删除接口,添加试卷部分接口(获取题库,获取题目,导入,导出功能),修复题库管理获取课程列表,分页器显示和切换问题,对接导入题库接口,添加试题接入查询分类,难度接口,修复试题渲染问题,修复编辑题目时选项数据无法正确显示的问题,添加试题编辑接口
This commit is contained in:
parent
3461499661
commit
1721ab50fc
523
docs/ExamInfo-API-Usage.md
Normal file
523
docs/ExamInfo-API-Usage.md
Normal file
@ -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`
|
112
src/api/examples/createPaper-example.ts
Normal file
112
src/api/examples/createPaper-example.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
180
src/api/examples/deletePaper-example.ts
Normal file
180
src/api/examples/deletePaper-example.ts
Normal file
@ -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: '删除失败,请检查网络连接' }
|
||||
}
|
||||
}
|
73
src/api/examples/getExamInfo-example.ts
Normal file
73
src/api/examples/getExamInfo-example.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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<ApiResponse<{ id: string; name: string }[]>> {
|
||||
try {
|
||||
// 调用现有的课程列表API,但只返回id和name字段
|
||||
const response = await ApiRequest.get<any>('/biz/course/list')
|
||||
// 调用教师端课程列表API
|
||||
const response = await ApiRequest.get<any>('/aiol/aiolCourse/teacher_list')
|
||||
console.log('✅ 获取课程列表成功:', response)
|
||||
|
||||
// 处理响应数据,只提取id和name
|
||||
@ -164,9 +166,9 @@ export class ExamApi {
|
||||
/**
|
||||
* 查询题库下题目
|
||||
*/
|
||||
static async getQuestionsByRepo(repoId: string): Promise<ApiResponse<Question[]>> {
|
||||
static async getQuestionsByRepo(repoId: string): Promise<ApiResponseWithResult<Question[]>> {
|
||||
console.log('🚀 查询题库下题目:', { repoId })
|
||||
const response = await ApiRequest.get<Question[]>(`/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<ApiResponse<Question>> {
|
||||
console.log('🚀 查询题目详情:', { questionId })
|
||||
const response = await ApiRequest.get<Question>(`/aiol/aiolRepo/repoList/${questionId}`)
|
||||
const response = await ApiRequest.get<Question>(`/aiol/aiolQuestion/queryById?id=${questionId}`)
|
||||
console.log('✅ 查询题目详情成功:', response)
|
||||
return response
|
||||
}
|
||||
@ -253,13 +255,23 @@ export class ExamApi {
|
||||
*/
|
||||
static async deleteQuestionOption(id: string): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 删除题目选项:', { id })
|
||||
const response = await ApiRequest.delete<string>('/gen/questionoption/questionOption/delete', {
|
||||
const response = await ApiRequest.delete<string>('/aiol/aiolQuestionOption/delete', {
|
||||
params: { id }
|
||||
})
|
||||
console.log('✅ 删除题目选项成功:', response)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题目选项列表
|
||||
*/
|
||||
static async getQuestionOptions(questionId: string): Promise<ApiResponse<any[]>> {
|
||||
console.log('🚀 获取题目选项列表:', { questionId })
|
||||
const response = await ApiRequest.get<any[]>(`/aiol/aiolQuestionOption/list?questionId=${questionId}`)
|
||||
console.log('✅ 获取题目选项列表成功:', response)
|
||||
return response
|
||||
}
|
||||
|
||||
// ========== 题目答案管理 ==========
|
||||
|
||||
/**
|
||||
@ -287,7 +299,7 @@ export class ExamApi {
|
||||
*/
|
||||
static async deleteQuestionAnswer(id: string): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 删除题目答案:', { id })
|
||||
const response = await ApiRequest.delete<string>('/gen/questionanswer/questionAnswer/delete', {
|
||||
const response = await ApiRequest.delete<string>('/aiol/aiolQuestionAnswer/delete', {
|
||||
params: { id }
|
||||
})
|
||||
console.log('✅ 删除题目答案成功:', response)
|
||||
@ -301,7 +313,7 @@ export class ExamApi {
|
||||
*/
|
||||
static async createQuestionRepo(data: CreateQuestionRepoRequest): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 添加题库题目关联:', data)
|
||||
const response = await ApiRequest.post<string>('/gen/questionrepo/questionRepo/add', data)
|
||||
const response = await ApiRequest.post<string>('/aiol/aiolQuestionRepo/add', data)
|
||||
console.log('✅ 添加题库题目关联成功:', response)
|
||||
return response
|
||||
}
|
||||
@ -311,7 +323,7 @@ export class ExamApi {
|
||||
*/
|
||||
static async updateQuestionRepo(data: UpdateQuestionRepoRequest): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 编辑题库题目关联:', data)
|
||||
const response = await ApiRequest.put<string>('/gen/questionrepo/questionRepo/edit', data)
|
||||
const response = await ApiRequest.put<string>('/aiol/aiolQuestionRepo/edit', data)
|
||||
console.log('✅ 编辑题库题目关联成功:', response)
|
||||
return response
|
||||
}
|
||||
@ -321,13 +333,370 @@ export class ExamApi {
|
||||
*/
|
||||
static async deleteQuestionRepo(id: string): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 删除题库题目关联:', { id })
|
||||
const response = await ApiRequest.delete<string>('/gen/questionrepo/questionRepo/delete', {
|
||||
const response = await ApiRequest.delete<string>('/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<ApiResponse<string>> {
|
||||
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<ApiResponse<string>> {
|
||||
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<ApiResponse<any[]>> {
|
||||
console.log('🚀 获取题目答案列表:', { questionId })
|
||||
const response = await ApiRequest.get<any[]>(`/aiol/aiolQuestionAnswer/list?questionId=${questionId}`)
|
||||
console.log('✅ 获取题目答案列表成功:', response)
|
||||
return response
|
||||
}
|
||||
|
||||
// ========== 分类和难度管理 ==========
|
||||
|
||||
/**
|
||||
* 获取题目分类列表
|
||||
*/
|
||||
static async getQuestionCategories(): Promise<ApiResponse<Array<{id: string, name: string}>>> {
|
||||
try {
|
||||
console.log('🚀 获取题目分类列表')
|
||||
const response = await ApiRequest.get<any>('/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<ApiResponse<Array<{id: string, name: string}>>> {
|
||||
try {
|
||||
console.log('🚀 获取题目难度列表')
|
||||
const response = await ApiRequest.get<any>('/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<ApiResponse<string>> {
|
||||
console.log('🚀 导入题目Excel文件:', { repoId })
|
||||
|
||||
try {
|
||||
const response = await ApiRequest.post<string>(`/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<Blob> {
|
||||
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<Blob> {
|
||||
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<ApiResponse<ExamInfo[]>> {
|
||||
console.log('🚀 获取教师名下的考试信息:', { userId })
|
||||
const response = await ApiRequest.get<ExamInfo[]>(`/aiol/aiolExam/getExamInfo?userId=${userId}`)
|
||||
console.log('✅ 获取教师名下的考试信息成功:', response)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取试卷列表
|
||||
*/
|
||||
@ -430,27 +969,132 @@ export class ExamApi {
|
||||
/**
|
||||
* 获取试卷详情
|
||||
*/
|
||||
static async getExamPaperDetail(id: string): Promise<ApiResponse<any>> {
|
||||
static async getExamPaperDetail(id: string): Promise<ApiResponse<{
|
||||
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
|
||||
}
|
||||
}>> {
|
||||
console.log('🚀 获取试卷详情:', id)
|
||||
const response = await ApiRequest.get<any>(`/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<ApiResponse<{
|
||||
success: boolean
|
||||
message: string
|
||||
result: any[]
|
||||
}>> {
|
||||
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<ApiResponse<string>> {
|
||||
console.log('🚀 添加试卷题目:', data)
|
||||
const response = await ApiRequest.post<string>(`/aiol/aiolPaperQuestion/add`, data)
|
||||
console.log('✅ 添加试卷题目成功:', response)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑试卷题目
|
||||
*/
|
||||
static async updateExamPaperQuestion(data: {
|
||||
id: string
|
||||
paperId: string
|
||||
questionId: string
|
||||
orderNo: number
|
||||
score: number
|
||||
}): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 编辑试卷题目:', data)
|
||||
const response = await ApiRequest.put<string>(`/aiol/aiolPaperQuestion/edit`, data)
|
||||
console.log('✅ 编辑试卷题目成功:', response)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除试卷题目
|
||||
*/
|
||||
static async deleteExamPaperQuestion(id: string): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 删除试卷题目:', id)
|
||||
const response = await ApiRequest.delete<string>(`/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<ApiResponse<string>> {
|
||||
console.log('🚀 创建试卷:', data)
|
||||
const response = await ApiRequest.post<string>('/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<string>('/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<ApiResponse<string>> {
|
||||
console.log('🚀 更新试卷:', { id, data })
|
||||
const response = await ApiRequest.put<string>(`/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<string>(`/aiol/aiolPaper/edit`, apiData)
|
||||
console.log('✅ 更新试卷成功:', response)
|
||||
return response
|
||||
}
|
||||
@ -478,19 +1139,64 @@ export class ExamApi {
|
||||
*/
|
||||
static async deleteExamPaper(id: string): Promise<ApiResponse<string>> {
|
||||
console.log('🚀 删除试卷:', id)
|
||||
const response = await ApiRequest.delete<string>(`/aiol/aiolExam/paperDelete/${id}`)
|
||||
const response = await ApiRequest.delete<string>(`/aiol/aiolPaper/delete?id=${id}`)
|
||||
console.log('✅ 删除试卷成功:', response)
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除试卷
|
||||
* 由于后端可能不支持批量删除接口,改为循环调用单个删除接口
|
||||
*/
|
||||
static async batchDeleteExamPapers(ids: string[]): Promise<ApiResponse<string>> {
|
||||
static async batchDeleteExamPapers(ids: string[]): Promise<ApiResponse<{
|
||||
success: number
|
||||
failed: number
|
||||
total: number
|
||||
errors: string[]
|
||||
}>> {
|
||||
console.log('🚀 批量删除试卷:', ids)
|
||||
const response = await ApiRequest.post<string>('/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<typeof results>
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 批量删除试卷失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,6 +94,11 @@ request.interceptors.response.use(
|
||||
})
|
||||
}
|
||||
|
||||
// 如果是blob响应,直接返回
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response
|
||||
}
|
||||
|
||||
// 处理不同的响应格式
|
||||
let normalizedData: ApiResponse
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -24,7 +24,8 @@
|
||||
<!-- 考试人数限制 -->
|
||||
<div class="setting-row">
|
||||
<label class="setting-label">考试人数</label>
|
||||
<n-input v-model:value="formData.maxParticipants" type="number" :min="1" placeholder="请输入考试人数上限" class="setting-input" />
|
||||
<n-input v-model:value="formData.maxParticipants" :min="1" placeholder="请输入考试人数上限"
|
||||
class="setting-input" />
|
||||
</div>
|
||||
<!-- 试卷分类 -->
|
||||
<div class="setting-row">
|
||||
@ -206,9 +207,12 @@
|
||||
<div class="advanced-setting">
|
||||
<label class="advanced-label">详分设置</label>
|
||||
<n-radio-group v-model:value="formData.detailScoreMode" class="detail-score-group">
|
||||
<n-radio value="question">填空题、简答题题目设为为主观题 <span class="tip">设为主观题后需教师手动批阅</span> </n-radio>
|
||||
<n-radio value="automatic">填空题、简答题不区分大小写 <span class="tip">勾选后,英文大写和小写都可以得分</span> </n-radio>
|
||||
<n-radio value="show_current">填空题、简答题忽略符号 <span class="tip">勾选后,答案内符号与标准答案不同也给分</span> </n-radio>
|
||||
<n-radio value="question">填空题、简答题题目设为为主观题 <span class="tip">设为主观题后需教师手动批阅</span>
|
||||
</n-radio>
|
||||
<n-radio value="automatic">填空题、简答题不区分大小写 <span class="tip">勾选后,英文大写和小写都可以得分</span>
|
||||
</n-radio>
|
||||
<n-radio value="show_current">填空题、简答题忽略符号 <span class="tip">勾选后,答案内符号与标准答案不同也给分</span>
|
||||
</n-radio>
|
||||
<n-radio value="show_all">多选题未全选对时得一半分 <span class="tip">不勾选时全选对才给分</span> </n-radio>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
@ -274,7 +278,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
import {
|
||||
createDiscreteApi,
|
||||
NModal,
|
||||
NInput,
|
||||
NDatePicker,
|
||||
NSelect,
|
||||
NRadioGroup,
|
||||
NRadio,
|
||||
NCheckbox,
|
||||
NButton,
|
||||
NDivider,
|
||||
NSwitch
|
||||
} from 'naive-ui';
|
||||
|
||||
// 创建独立的 message API
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
@ -286,12 +302,12 @@ interface ExamSettings {
|
||||
endTime: number | null;
|
||||
category: 'exam' | 'practice';
|
||||
timeLimit: 'unlimited' | 'limited' | 'no_limit';
|
||||
timeLimitValue: number;
|
||||
timeLimitValue: string;
|
||||
examTimes: 'unlimited' | 'limited' | 'each_day';
|
||||
examTimesValue: number;
|
||||
examTimesValue: string;
|
||||
dailyLimit: number;
|
||||
chapter: string;
|
||||
passScore: number;
|
||||
passScore: string;
|
||||
participants: 'all' | 'by_school';
|
||||
selectedClasses: string[];
|
||||
instructions: string;
|
||||
@ -299,11 +315,11 @@ interface ExamSettings {
|
||||
// 考试模式专用
|
||||
enforceOrder: boolean;
|
||||
enforceInstructions: boolean;
|
||||
readingTime: number;
|
||||
readingTime: string;
|
||||
submitSettings: {
|
||||
allowEarlySubmit: boolean;
|
||||
};
|
||||
gradingDelay: number;
|
||||
gradingDelay: string;
|
||||
scoreDisplay: 'show_all' | 'show_score' | 'hide_all';
|
||||
detailedSettings: {
|
||||
showQuestions: boolean;
|
||||
@ -311,15 +327,15 @@ interface ExamSettings {
|
||||
showSubmissionTime: boolean;
|
||||
};
|
||||
timerEnabled: boolean;
|
||||
timerDuration: number;
|
||||
timerDuration: string;
|
||||
answerType: 'auto_save' | 'manual_save' | 'multiple_submit';
|
||||
detailScoreMode: 'question' | 'automatic' | 'show_current' | 'show_all';
|
||||
showRanking: boolean; // 展示排名
|
||||
courseProgress: number; // 作答要求的课程进度
|
||||
courseProgress: string; // 作答要求的课程进度
|
||||
|
||||
// 练习模式专用
|
||||
correctnessMode: 'no_limit' | 'limit_wrong';
|
||||
wrongLimit: number;
|
||||
wrongLimit: string;
|
||||
practiceSettings: {
|
||||
showCorrectAnswer: boolean;
|
||||
showWrongAnswer: boolean;
|
||||
@ -329,7 +345,7 @@ interface ExamSettings {
|
||||
};
|
||||
paperMode: 'show_all' | 'show_current' | 'hide_all';
|
||||
// 新增考试人数限制字段
|
||||
maxParticipants: number | null;
|
||||
maxParticipants: string | null;
|
||||
}
|
||||
|
||||
// Props 定义
|
||||
@ -361,12 +377,12 @@ const formData = ref<ExamSettings>({
|
||||
endTime: null,
|
||||
category: 'exam',
|
||||
timeLimit: 'limited',
|
||||
timeLimitValue: 0,
|
||||
timeLimitValue: '0',
|
||||
examTimes: 'unlimited',
|
||||
examTimesValue: 1,
|
||||
examTimesValue: '1',
|
||||
dailyLimit: 1,
|
||||
chapter: '',
|
||||
passScore: 60,
|
||||
passScore: '60',
|
||||
participants: 'all',
|
||||
selectedClasses: [],
|
||||
instructions: '',
|
||||
@ -374,11 +390,11 @@ const formData = ref<ExamSettings>({
|
||||
// 考试模式专用
|
||||
enforceOrder: false,
|
||||
enforceInstructions: false,
|
||||
readingTime: 10,
|
||||
readingTime: '10',
|
||||
submitSettings: {
|
||||
allowEarlySubmit: true,
|
||||
},
|
||||
gradingDelay: 60,
|
||||
gradingDelay: '60',
|
||||
scoreDisplay: 'show_all',
|
||||
detailedSettings: {
|
||||
showQuestions: false,
|
||||
@ -386,15 +402,15 @@ const formData = ref<ExamSettings>({
|
||||
showSubmissionTime: false,
|
||||
},
|
||||
timerEnabled: false,
|
||||
timerDuration: 10,
|
||||
timerDuration: '10',
|
||||
answerType: 'auto_save',
|
||||
detailScoreMode: 'question',
|
||||
showRanking: false,
|
||||
courseProgress: 0,
|
||||
courseProgress: '0',
|
||||
|
||||
// 练习模式专用
|
||||
correctnessMode: 'no_limit',
|
||||
wrongLimit: 10,
|
||||
wrongLimit: '10',
|
||||
practiceSettings: {
|
||||
showCorrectAnswer: false,
|
||||
showWrongAnswer: false,
|
||||
|
@ -1,50 +1,62 @@
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
class="question-bank-modal"
|
||||
preset="card"
|
||||
:mask-closable="false"
|
||||
:closable="false"
|
||||
<n-modal v-model:show="showModal" class="question-bank-modal" preset="card" :mask-closable="false" :closable="false"
|
||||
:style="{ width: '1200px' }">
|
||||
|
||||
<div class="header">
|
||||
<span class="header-title">题库</span>
|
||||
</div>
|
||||
<n-divider />
|
||||
|
||||
|
||||
<div class="question-bank-content">
|
||||
<!-- 筛选条件 -->
|
||||
<div class="filter-section">
|
||||
<div class="filter-row">
|
||||
<div class="filter-item">
|
||||
<label>选择题库:</label>
|
||||
<n-select v-model:value="selectedRepo" placeholder="全部题库" :options="repoOptions"
|
||||
style="width: 200px" @update:value="handleRepoChange" />
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>试题分类:</label>
|
||||
<n-select
|
||||
v-model:value="filters.category"
|
||||
placeholder="全部"
|
||||
:options="categoryOptions"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<n-select v-model:value="filters.category" placeholder="全部" :options="categoryOptions"
|
||||
style="width: 150px" @update:value="handleFilterChange" />
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>试题难度:</label>
|
||||
<n-select
|
||||
v-model:value="filters.difficulty"
|
||||
placeholder="全部"
|
||||
:options="difficultyOptions"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<n-select v-model:value="filters.difficulty" placeholder="全部" :options="difficultyOptions"
|
||||
style="width: 150px" @update:value="handleFilterChange" />
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>试题题型:</label>
|
||||
<n-select
|
||||
v-model:value="filters.type"
|
||||
placeholder="全部"
|
||||
:options="typeOptions"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<n-select v-model:value="filters.type" placeholder="全部" :options="typeOptions"
|
||||
style="width: 150px" @update:value="handleFilterChange" />
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<span class="tip">已全部加载,共{{ pagination.itemCount }}试题</span>
|
||||
<span class="tip">
|
||||
{{ selectedRepo ? `已加载题库题目,共${pagination.itemCount}试题` : '请先选择题库' }}
|
||||
</span>
|
||||
<n-button type="default" @click="resetFilters" style="margin-right: 8px;">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
重置筛选
|
||||
</n-button>
|
||||
<n-button type="default" @click="exportQuestions" style="margin-right: 8px;">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
导出模板
|
||||
</n-button>
|
||||
<n-button type="primary" @click="addNewQuestion">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
@ -53,23 +65,60 @@
|
||||
</template>
|
||||
导入试题
|
||||
</n-button>
|
||||
<n-tooltip trigger="hover" placement="top">
|
||||
<template #trigger>
|
||||
<n-button type="info" quaternary circle size="small" style="margin-left: 8px;">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<div style="max-width: 300px; line-height: 1.5;">
|
||||
<div style="font-weight: bold; margin-bottom: 8px;">Excel导入格式说明:</div>
|
||||
<div>• 必填字段:题目内容、正确答案</div>
|
||||
<div>• 正确答案格式:A、B、C或A,B,C</div>
|
||||
<div>• 支持题型:单选题、多选题、判断题</div>
|
||||
<div>• 文件格式:.xlsx或.xls</div>
|
||||
<div>• 文件大小:不超过10MB</div>
|
||||
</div>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 题目列表 -->
|
||||
<div class="question-list-section">
|
||||
<n-data-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:data="questionList"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
:row-key="(row: any) => row.id"
|
||||
:checked-row-keys="selectedRowKeys"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
striped
|
||||
/>
|
||||
<n-data-table ref="tableRef" :columns="columns" :data="questionList" :pagination="pagination"
|
||||
:loading="loading" :row-key="(row: any) => row.id" :checked-row-keys="selectedRowKeys"
|
||||
@update:checked-row-keys="handleCheck" striped>
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<div v-if="!selectedRepo" class="empty-tip">
|
||||
<n-icon size="48" color="#ccc">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
<p>请先选择题库查看题目</p>
|
||||
</div>
|
||||
<div v-else class="empty-tip">
|
||||
<n-icon size="48" color="#ccc">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
<p>该题库暂无题目</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</div>
|
||||
|
||||
<!-- 已选择题目统计 -->
|
||||
@ -92,11 +141,14 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
import { Add } from '@vicons/ionicons5';
|
||||
import { ExamApi } from '@/api/modules/exam';
|
||||
import { CourseApi } from '@/api/modules/course';
|
||||
import type { Repo, Question, CourseCategory, CourseDifficulty } from '@/api/types';
|
||||
|
||||
// 创建独立的 message API
|
||||
const { message } = createDiscreteApi(['message']);
|
||||
|
||||
// 题目接口定义
|
||||
// 题目接口定义(用于显示)
|
||||
interface QuestionItem {
|
||||
id: string;
|
||||
number: number;
|
||||
@ -109,6 +161,8 @@ interface QuestionItem {
|
||||
createTime: string;
|
||||
// 题目详细内容
|
||||
content?: any;
|
||||
// 原始题目数据
|
||||
originalData?: Question;
|
||||
}
|
||||
|
||||
// Props 定义
|
||||
@ -130,7 +184,7 @@ const emit = defineEmits<Emits>();
|
||||
// 内部状态
|
||||
const showModal = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value)
|
||||
set: (value: boolean) => emit('update:visible', value)
|
||||
});
|
||||
|
||||
// 筛选条件
|
||||
@ -138,62 +192,125 @@ const filters = ref({
|
||||
category: '',
|
||||
difficulty: '',
|
||||
type: props.questionType || '',
|
||||
keyword: ''
|
||||
keyword: '',
|
||||
repoId: '' // 添加题库筛选
|
||||
});
|
||||
|
||||
// 题库列表
|
||||
const repoList = ref<Repo[]>([]);
|
||||
const selectedRepo = ref<string>('');
|
||||
|
||||
// 筛选选项
|
||||
const categoryOptions = ref([
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '计算机基础', value: 'computer' },
|
||||
{ label: '数学', value: 'math' },
|
||||
{ label: '英语', value: 'english' }
|
||||
{ label: '全部', value: '' }
|
||||
]);
|
||||
|
||||
// 分类列表
|
||||
const categoryList = ref<CourseCategory[]>([]);
|
||||
|
||||
// 题库选项(动态加载)
|
||||
const repoOptions = computed(() => [
|
||||
{ label: '全部题库', value: '' },
|
||||
...repoList.value.map((repo: Repo) => ({
|
||||
label: `${repo.title} (${repo.questionCount || 0}题)`,
|
||||
value: repo.id
|
||||
}))
|
||||
]);
|
||||
|
||||
const difficultyOptions = ref([
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '易', value: 'easy' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '难', value: 'hard' }
|
||||
{ label: '简单', value: '1' },
|
||||
{ label: '中等', value: '2' },
|
||||
{ label: '困难', value: '3' }
|
||||
]);
|
||||
|
||||
const typeOptions = ref([
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '单选题', value: 'single_choice' },
|
||||
{ label: '多选题', value: 'multiple_choice' },
|
||||
{ label: '判断题', value: 'true_false' },
|
||||
{ label: '填空题', value: 'fill_blank' },
|
||||
{ label: '简答题', value: 'short_answer' }
|
||||
{ label: '单选题', value: '0' },
|
||||
{ label: '多选题', value: '1' },
|
||||
{ label: '判断题', value: '2' },
|
||||
{ label: '填空题', value: '3' },
|
||||
{ label: '简答题', value: '4' },
|
||||
{ label: '复合题', value: '5' }
|
||||
]);
|
||||
|
||||
// 表格配置
|
||||
const loading = ref(false);
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
|
||||
// 题目列表数据 (模拟数据)
|
||||
// 题目列表数据
|
||||
const questionList = ref<QuestionItem[]>([]);
|
||||
const allQuestions = ref<QuestionItem[]>([]); // 存储所有题目,用于筛选
|
||||
|
||||
// 筛选后的题目列表
|
||||
const filteredQuestions = computed(() => {
|
||||
let filtered = allQuestions.value;
|
||||
|
||||
// 按分类筛选
|
||||
if (filters.value.category) {
|
||||
filtered = filtered.filter(q => q.category === filters.value.category);
|
||||
}
|
||||
|
||||
// 按难度筛选
|
||||
if (filters.value.difficulty) {
|
||||
const difficultyMap: { [key: string]: string } = {
|
||||
'1': '简单',
|
||||
'2': '中等',
|
||||
'3': '困难'
|
||||
};
|
||||
const targetDifficulty = difficultyMap[filters.value.difficulty];
|
||||
filtered = filtered.filter(q => q.difficulty === targetDifficulty);
|
||||
}
|
||||
|
||||
// 按题型筛选
|
||||
if (filters.value.type) {
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'0': '单选题',
|
||||
'1': '多选题',
|
||||
'2': '判断题',
|
||||
'3': '填空题',
|
||||
'4': '简答题',
|
||||
'5': '复合题'
|
||||
};
|
||||
const targetType = typeMap[filters.value.type];
|
||||
filtered = filtered.filter(q => q.type === targetType);
|
||||
}
|
||||
|
||||
// 按关键词筛选
|
||||
if (filters.value.keyword) {
|
||||
const keyword = filters.value.keyword.toLowerCase();
|
||||
filtered = filtered.filter(q =>
|
||||
q.title.toLowerCase().includes(keyword) ||
|
||||
q.creator.toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 生成模拟数据
|
||||
const generateMockData = () => {
|
||||
const mockData: QuestionItem[] = [];
|
||||
const types = ['单选题', '多选题', '判断题', '填空题', '简答题'];
|
||||
const difficulties = ['易', '中', '难'];
|
||||
const categories = ['计算机基础', '数学', '英语'];
|
||||
|
||||
for (let i = 1; i <= 15; i++) {
|
||||
mockData.push({
|
||||
id: `question_${i}`,
|
||||
number: i,
|
||||
title: `在数据库的三级模式结构中,内模式有...`,
|
||||
type: types[Math.floor(Math.random() * types.length)],
|
||||
difficulty: difficulties[Math.floor(Math.random() * difficulties.length)],
|
||||
category: categories[Math.floor(Math.random() * categories.length)],
|
||||
score: 10,
|
||||
creator: '王建国',
|
||||
createTime: '2025.08.20 09:20'
|
||||
});
|
||||
}
|
||||
return mockData;
|
||||
};
|
||||
// 生成模拟数据(暂时未使用)
|
||||
// const generateMockData = () => {
|
||||
// const mockData: QuestionItem[] = [];
|
||||
// const types = ['单选题', '多选题', '判断题', '填空题', '简答题'];
|
||||
// const difficulties = ['易', '中', '难'];
|
||||
// const categories = ['计算机基础', '数学', '英语'];
|
||||
//
|
||||
// for (let i = 1; i <= 15; i++) {
|
||||
// mockData.push({
|
||||
// id: `question_${i}`,
|
||||
// number: i,
|
||||
// title: `在数据库的三级模式结构中,内模式有...`,
|
||||
// type: types[Math.floor(Math.random() * types.length)],
|
||||
// difficulty: difficulties[Math.floor(Math.random() * difficulties.length)],
|
||||
// category: categories[Math.floor(Math.random() * categories.length)],
|
||||
// score: 10,
|
||||
// creator: '王建国',
|
||||
// createTime: '2025.08.20 09:20'
|
||||
// });
|
||||
// }
|
||||
// return mockData;
|
||||
// };
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
@ -259,16 +376,18 @@ const pagination = ref({
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50],
|
||||
itemCount: 0,
|
||||
showQuickJumper: true,
|
||||
displayOrder: ['size-picker', 'pages', 'quick-jumper'],
|
||||
onChange: (page: number) => {
|
||||
pagination.value.page = page;
|
||||
loadQuestions();
|
||||
updateCurrentPageQuestions();
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.value.pageSize = pageSize;
|
||||
pagination.value.page = 1;
|
||||
loadQuestions();
|
||||
updateCurrentPageQuestions();
|
||||
},
|
||||
prefix: ({ itemCount }: { itemCount: number }) => `共${itemCount}试题`
|
||||
prefix: ({ itemCount }: { itemCount: number }) => `共${itemCount}题`
|
||||
});
|
||||
|
||||
// 处理复选框选择
|
||||
@ -276,24 +395,317 @@ const handleCheck = (rowKeys: string[]) => {
|
||||
selectedRowKeys.value = rowKeys;
|
||||
};
|
||||
|
||||
// 更新当前页的题目
|
||||
const updateCurrentPageQuestions = () => {
|
||||
const filtered = filteredQuestions.value;
|
||||
const startIndex = (pagination.value.page - 1) * pagination.value.pageSize;
|
||||
const endIndex = startIndex + pagination.value.pageSize;
|
||||
questionList.value = filtered.slice(startIndex, endIndex);
|
||||
console.log('📄 更新当前页题目 - 页码:', pagination.value.page, '每页:', pagination.value.pageSize, '显示题目数:', questionList.value.length);
|
||||
};
|
||||
|
||||
// 处理题库选择变化
|
||||
const handleRepoChange = (repoId: string) => {
|
||||
selectedRepo.value = repoId;
|
||||
filters.value.repoId = repoId;
|
||||
loadQuestions();
|
||||
};
|
||||
|
||||
// 筛选条件变化处理
|
||||
const handleFilterChange = () => {
|
||||
const filtered = filteredQuestions.value;
|
||||
pagination.value.itemCount = filtered.length;
|
||||
pagination.value.page = 1; // 重置到第一页
|
||||
|
||||
// 计算当前页的题目
|
||||
const startIndex = (pagination.value.page - 1) * pagination.value.pageSize;
|
||||
const endIndex = startIndex + pagination.value.pageSize;
|
||||
questionList.value = filtered.slice(startIndex, endIndex);
|
||||
|
||||
console.log('📊 筛选后分页器更新 - 题目总数:', pagination.value.itemCount);
|
||||
};
|
||||
|
||||
// 重置筛选条件
|
||||
const resetFilters = () => {
|
||||
filters.value = {
|
||||
category: '',
|
||||
difficulty: '',
|
||||
type: props.questionType || '',
|
||||
keyword: '',
|
||||
repoId: selectedRepo.value
|
||||
};
|
||||
handleFilterChange();
|
||||
};
|
||||
|
||||
// 加载分类选项
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
const response = await CourseApi.getCategories();
|
||||
if (response.data && response.data.length > 0) {
|
||||
categoryList.value = response.data;
|
||||
categoryOptions.value = [
|
||||
{ label: '全部', value: '' },
|
||||
...response.data.map((category: CourseCategory) => ({
|
||||
label: category.name,
|
||||
value: category.name
|
||||
}))
|
||||
];
|
||||
console.log('✅ 加载分类选项成功:', categoryOptions.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载分类选项失败:', error);
|
||||
// 使用默认分类选项
|
||||
categoryOptions.value = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '计算机基础', value: '计算机基础' },
|
||||
{ label: '数学', value: '数学' },
|
||||
{ label: '英语', value: '英语' }
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// 加载难度选项
|
||||
const loadDifficulties = async () => {
|
||||
try {
|
||||
const response = await CourseApi.getDifficulties();
|
||||
if (response.data && response.data.length > 0) {
|
||||
difficultyOptions.value = [
|
||||
{ label: '全部', value: '' },
|
||||
...response.data.map((difficulty: CourseDifficulty) => ({
|
||||
label: difficulty.name,
|
||||
value: difficulty.id
|
||||
}))
|
||||
];
|
||||
console.log('✅ 加载难度选项成功:', difficultyOptions.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载难度选项失败:', error);
|
||||
// 使用默认难度选项
|
||||
difficultyOptions.value = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '简单', value: '1' },
|
||||
{ label: '中等', value: '2' },
|
||||
{ label: '困难', value: '3' }
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// 加载题库列表
|
||||
const loadRepos = async () => {
|
||||
try {
|
||||
const response = await ExamApi.getCourseRepoList();
|
||||
if (response.data && response.data.result) {
|
||||
repoList.value = response.data.result;
|
||||
console.log('✅ 加载题库列表成功:', repoList.value);
|
||||
|
||||
// 如果还没有选择题库,自动选择第一个题库
|
||||
if (!selectedRepo.value && repoList.value.length > 0) {
|
||||
selectedRepo.value = repoList.value[0].id;
|
||||
console.log('🔄 自动选择第一个题库:', selectedRepo.value);
|
||||
// 自动加载该题库的题目
|
||||
await loadQuestions();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载题库列表失败:', error);
|
||||
message.error('加载题库列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载题目列表
|
||||
const loadQuestions = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
questionList.value = generateMockData();
|
||||
pagination.value.itemCount = questionList.value.length;
|
||||
if (selectedRepo.value) {
|
||||
// 根据选择的题库加载题目
|
||||
console.log('🔍 正在加载题库题目:', selectedRepo.value);
|
||||
const response = await ExamApi.getQuestionsByRepo(selectedRepo.value);
|
||||
console.log('✅ 题库题目响应:', response);
|
||||
|
||||
if (response.data && response.data.result && response.data.result.length > 0) {
|
||||
const questions = response.data.result.map((q: Question, index: number) => ({
|
||||
id: q.id,
|
||||
number: index + 1,
|
||||
title: q.content || '无标题',
|
||||
type: ExamApi.getQuestionTypeText(q.type || 0),
|
||||
difficulty: ExamApi.getDifficultyText(q.difficulty || 1),
|
||||
category: '计算机基础', // 暂时固定
|
||||
score: q.score || 5,
|
||||
creator: q.createBy || '未知',
|
||||
createTime: q.createTime ? new Date(q.createTime).toLocaleString() : '未知时间',
|
||||
originalData: q
|
||||
}));
|
||||
allQuestions.value = questions;
|
||||
|
||||
// 应用筛选条件并更新当前页
|
||||
const filtered = filteredQuestions.value;
|
||||
pagination.value.itemCount = filtered.length;
|
||||
|
||||
// 计算当前页的题目
|
||||
const startIndex = (pagination.value.page - 1) * pagination.value.pageSize;
|
||||
const endIndex = startIndex + pagination.value.pageSize;
|
||||
questionList.value = filtered.slice(startIndex, endIndex);
|
||||
|
||||
console.log('✅ 题目列表加载成功:', questionList.value);
|
||||
console.log('📊 筛选后题目总数:', filtered.length);
|
||||
} else {
|
||||
// 如果题库没有题目,显示空列表
|
||||
allQuestions.value = [];
|
||||
questionList.value = [];
|
||||
console.log('⚠️ 该题库暂无题目');
|
||||
}
|
||||
} else {
|
||||
// 如果没有选择题库,显示提示信息
|
||||
questionList.value = [];
|
||||
console.log('ℹ️ 请先选择题库');
|
||||
}
|
||||
|
||||
console.log('📊 分页器更新 - 题目总数:', pagination.value.itemCount);
|
||||
} catch (error) {
|
||||
console.error('加载题目失败:', error);
|
||||
message.error('加载题目失败');
|
||||
questionList.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加新题目
|
||||
// 导入试题
|
||||
const addNewQuestion = () => {
|
||||
message.info('导入试题功能待开发');
|
||||
// 创建文件输入元素
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.xlsx,.xls';
|
||||
input.style.display = 'none';
|
||||
|
||||
input.onchange = async (event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!selectedRepo.value) {
|
||||
message.error('请先选择题库');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
const allowedTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
||||
'application/vnd.ms-excel' // .xls
|
||||
];
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
message.error('请选择Excel文件(.xlsx或.xls格式)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小 (限制10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.error('文件大小不能超过10MB');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
message.loading('正在导入试题,请稍候...', { duration: 0 });
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await ExamApi.importQuestions(selectedRepo.value, formData);
|
||||
|
||||
if (response.data && typeof response.data === 'object' && 'success' in response.data && (response.data as any).success) {
|
||||
message.destroyAll();
|
||||
message.success('试题导入成功!');
|
||||
|
||||
// 重新加载题目列表
|
||||
await loadQuestions();
|
||||
} else {
|
||||
message.destroyAll();
|
||||
message.error('导入失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.destroyAll();
|
||||
console.error('导入试题失败:', error);
|
||||
|
||||
// 解析错误信息
|
||||
let errorMessage = '导入失败,请检查文件格式';
|
||||
|
||||
if (error.message) {
|
||||
if (error.message.includes('getCorrectAnswers()') && error.message.includes('null')) {
|
||||
errorMessage = '导入失败:Excel文件中的"正确答案"列不能为空,请检查文件内容';
|
||||
} else if (error.message.includes('split')) {
|
||||
errorMessage = '导入失败:Excel文件格式不正确,请检查数据格式';
|
||||
} else if (error.message.includes('Cannot invoke')) {
|
||||
errorMessage = '导入失败:Excel文件数据格式不正确,请检查必填字段';
|
||||
} else if (error.message.includes('null')) {
|
||||
errorMessage = '导入失败:Excel文件中存在空值,请检查必填字段';
|
||||
} else {
|
||||
errorMessage = `导入失败:${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
message.error(errorMessage);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 触发文件选择
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
document.body.removeChild(input);
|
||||
};
|
||||
|
||||
// 导出题目模板
|
||||
const exportQuestions = async () => {
|
||||
if (!selectedRepo.value) {
|
||||
message.error('请先选择题库');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
message.loading('正在导出模板,请稍候...', { duration: 0 });
|
||||
|
||||
const response = await ExamApi.exportQuestions(selectedRepo.value);
|
||||
|
||||
// 检查响应是否为有效的Blob对象
|
||||
if (!response || !(response instanceof Blob)) {
|
||||
throw new Error('服务器返回的数据格式不正确');
|
||||
}
|
||||
|
||||
// 检查blob大小
|
||||
if (response.size === 0) {
|
||||
throw new Error('导出的文件为空');
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(response);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
// 获取题库名称作为文件名
|
||||
const repo = repoList.value.find(r => r.id === selectedRepo.value);
|
||||
const fileName = repo ? `${repo.title}_题目模板.xlsx` : '题目模板.xlsx';
|
||||
link.download = fileName;
|
||||
|
||||
// 触发下载
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
// 清理URL对象
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
message.destroyAll();
|
||||
message.success('模板导出成功!');
|
||||
} catch (error: any) {
|
||||
message.destroyAll();
|
||||
console.error('导出模板失败:', error);
|
||||
message.error(error.message || '导出失败,请稍后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 取消选择
|
||||
@ -304,27 +716,36 @@ const cancelSelection = () => {
|
||||
|
||||
// 确认选择
|
||||
const confirmSelection = () => {
|
||||
const selectedQuestions = questionList.value.filter(q => selectedRowKeys.value.includes(q.id));
|
||||
const selectedQuestions = questionList.value.filter((q: QuestionItem) => selectedRowKeys.value.includes(q.id));
|
||||
emit('confirm', selectedQuestions);
|
||||
selectedRowKeys.value = [];
|
||||
};
|
||||
|
||||
// 监听visible变化,重置状态
|
||||
watch(() => props.visible, (visible) => {
|
||||
watch(() => props.visible, async (visible: boolean) => {
|
||||
if (visible) {
|
||||
loadQuestions();
|
||||
selectedRowKeys.value = [];
|
||||
// 如果指定了题型,设置筛选条件
|
||||
if (props.questionType) {
|
||||
filters.value.type = props.questionType;
|
||||
}
|
||||
// 并行加载所有选项数据
|
||||
await Promise.all([
|
||||
loadCategories(),
|
||||
loadDifficulties(),
|
||||
loadRepos()
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
if (props.visible) {
|
||||
loadQuestions();
|
||||
Promise.all([
|
||||
loadCategories(),
|
||||
loadDifficulties(),
|
||||
loadRepos()
|
||||
]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -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 {
|
||||
|
@ -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: '试卷分析' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -10,7 +10,7 @@
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<h1>添加试卷</h1>
|
||||
<h1>{{ isEditMode ? '编辑试卷' : '添加试卷' }}</h1>
|
||||
<span v-if="isAutoSaved" class="auto-save-indicator">
|
||||
<n-icon size="14" color="#52c41a">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
@ -182,7 +182,7 @@
|
||||
</div>
|
||||
<div class="sub-footer-item">
|
||||
题目必填:<n-select v-model:value="subQuestion.required" size="small" style="width: 80px"
|
||||
:options="[{ label: '是', value: true }, { label: '否', value: false }]" />
|
||||
:options="[{ label: '是', value: 'true' }, { label: '否', value: 'false' }]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -249,6 +249,17 @@
|
||||
<!-- 右侧按钮 -->
|
||||
<div class="footer-right">
|
||||
<n-space>
|
||||
<n-button strong type="primary" secondary size="large" @click="clearExamForm">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
</template>
|
||||
清除内容
|
||||
</n-button>
|
||||
<n-button strong type="primary" secondary size="large">
|
||||
取消
|
||||
</n-button>
|
||||
@ -277,7 +288,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
import {
|
||||
createDiscreteApi,
|
||||
NSpace,
|
||||
NButton,
|
||||
NIcon,
|
||||
NCard,
|
||||
NTag,
|
||||
NRow,
|
||||
NInput,
|
||||
NSelect,
|
||||
NInputNumber,
|
||||
NPopselect
|
||||
} from 'naive-ui';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { AddCircle, SettingsOutline, TrashOutline, BookSharp, ArrowBackOutline } from '@vicons/ionicons5'
|
||||
import BatchSetScoreModal from '@/components/admin/ExamComponents/BatchSetScoreModal.vue';
|
||||
@ -291,8 +314,8 @@ 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'])
|
||||
// 创建独立的 dialog 和 message API
|
||||
const { dialog, message } = createDiscreteApi(['dialog', 'message'])
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
@ -357,7 +380,7 @@ interface SubQuestion {
|
||||
title: string; // 题目内容
|
||||
score: number; // 分值
|
||||
difficulty: 'easy' | 'medium' | 'hard'; // 难度
|
||||
required: boolean; // 题目必填
|
||||
required: string; // 题目必填
|
||||
|
||||
// 选择题相关字段(适配题型组件)
|
||||
options?: ChoiceOption[]; // 选择题选项
|
||||
@ -429,6 +452,10 @@ const difficultyOptions = ref([
|
||||
{ label: '困难', value: 'hard' }
|
||||
])
|
||||
|
||||
// 编辑模式状态
|
||||
const isEditMode = ref(false);
|
||||
const examId = ref<string>('');
|
||||
|
||||
const examForm = reactive({
|
||||
title: '',
|
||||
type: 1, // 1: 固定试卷组, 2: 随机抽题组卷
|
||||
@ -461,6 +488,119 @@ const changeType = (e: number) => {
|
||||
examForm.type = e;
|
||||
}
|
||||
|
||||
// 加载试卷详情
|
||||
const loadExamDetail = async (id: string) => {
|
||||
try {
|
||||
console.log('🚀 加载试卷详情:', id);
|
||||
|
||||
// 并行加载试卷基本信息和题目信息
|
||||
const [examResponse, questionsResponse] = await Promise.all([
|
||||
ExamApi.getExamPaperDetail(id),
|
||||
ExamApi.getExamPaperQuestions(id).catch(() => ({ data: { success: true, result: [] } })) // 如果题目API失败,使用空数组
|
||||
]);
|
||||
|
||||
console.log('📊 试卷详情API响应:', examResponse);
|
||||
console.log('📊 试卷题目API响应:', questionsResponse);
|
||||
|
||||
if (examResponse.data && examResponse.data.success) {
|
||||
const examData = examResponse.data.result;
|
||||
|
||||
// 填充试卷基本信息
|
||||
examForm.title = examData.title || '';
|
||||
examForm.type = examData.generateMode === 1 ? 2 : 1; // 1: 固定试卷组, 2: 随机抽题组卷
|
||||
examForm.description = (examData as any).description || '';
|
||||
examForm.totalScore = examData.totalScore || 0;
|
||||
examForm.duration = (examData as any).duration || 60;
|
||||
examForm.passScore = examData.passScore || 60;
|
||||
examForm.instructions = (examData as any).instructions || '';
|
||||
examForm.useAIGrading = examData.requireReview === 1;
|
||||
|
||||
// 加载题目信息
|
||||
if (questionsResponse.data && questionsResponse.data.success && (questionsResponse.data.result as any).records && (questionsResponse.data.result as any).records.length > 0) {
|
||||
console.log('📊 发现题目关联数据,开始处理');
|
||||
console.log('📊 题目关联数据详情:', (questionsResponse.data.result as any).records);
|
||||
|
||||
// 获取题目关联数据
|
||||
const questionRelations = (questionsResponse.data.result as any).records;
|
||||
|
||||
// 根据questionId获取题目详细信息
|
||||
const questionDetails = await Promise.all(
|
||||
questionRelations.map(async (relation: any) => {
|
||||
try {
|
||||
console.log('📊 获取题目详情:', relation.questionId);
|
||||
const questionDetail = await ExamApi.getQuestionDetail(relation.questionId);
|
||||
if (questionDetail.data && (questionDetail.data as any).success) {
|
||||
return {
|
||||
...(questionDetail.data as any).result,
|
||||
paperQuestionId: relation.id,
|
||||
orderNo: relation.orderNo,
|
||||
score: relation.score
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('❌ 获取题目详情失败:', relation.questionId, error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 过滤掉获取失败的题目
|
||||
const validQuestions = questionDetails.filter((q: any) => q !== null);
|
||||
console.log('📊 有效题目数据:', validQuestions);
|
||||
|
||||
// 将题目数据转换为表单格式
|
||||
examForm.questions = validQuestions.map((question: any, index: number) => {
|
||||
console.log('📊 处理题目:', question);
|
||||
|
||||
// 构建大题结构
|
||||
const bigQuestion = {
|
||||
id: question.paperQuestionId || `big_question_${Date.now()}_${index}`,
|
||||
title: question.title || `大题 ${index + 1}`,
|
||||
type: 'composite',
|
||||
description: question.description || '',
|
||||
sort: question.orderNo || index + 1,
|
||||
totalScore: question.score || 0,
|
||||
subQuestions: [{
|
||||
id: question.id || `sub_question_${Date.now()}_${index}`,
|
||||
title: question.content || question.title || `题目 ${index + 1}`,
|
||||
type: convertNumberToQuestionType(question.type || 0),
|
||||
score: question.score || 0,
|
||||
difficulty: question.difficulty || 1,
|
||||
explanation: question.analysis || '',
|
||||
createTime: question.createTime || new Date().toISOString(),
|
||||
required: 'true',
|
||||
options: question.options || [],
|
||||
correctAnswer: question.correctAnswer || '',
|
||||
analysis: question.analysis || ''
|
||||
}],
|
||||
createTime: question.createTime || new Date().toISOString()
|
||||
};
|
||||
|
||||
console.log('📊 构建的大题结构:', bigQuestion);
|
||||
return bigQuestion;
|
||||
});
|
||||
} else {
|
||||
// 如果没有题目数据,保持默认的大题结构
|
||||
console.log('📝 试卷暂无题目数据,使用默认结构');
|
||||
}
|
||||
|
||||
console.log('✅ 试卷详情加载成功:', examData);
|
||||
} else {
|
||||
message.error((examResponse.data as any)?.message || '加载试卷详情失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ 加载试卷详情失败:', error);
|
||||
|
||||
if (error.response?.status === 404) {
|
||||
message.error('试卷不存在或已删除');
|
||||
router.back();
|
||||
} else {
|
||||
message.error('加载试卷详情失败,请重试');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addQuestion = (index: number) => {
|
||||
const questionType = questionTypeValue.value as QuestionType;
|
||||
const newSubQuestion: SubQuestion = {
|
||||
@ -469,7 +609,7 @@ const addQuestion = (index: number) => {
|
||||
title: '',
|
||||
score: 5, // 默认分值
|
||||
difficulty: 'medium',
|
||||
required: true, // 默认必填
|
||||
required: 'true', // 默认必填
|
||||
explanation: '', // 初始化解析字段
|
||||
textAnswer: '', // 初始化文本答案字段
|
||||
// 为所有可能的字段提供默认值
|
||||
@ -649,11 +789,11 @@ const updateCompositeData = (subQuestion: SubQuestion, compositeData: any) => {
|
||||
explanation: csq.explanation,
|
||||
createTime: new Date().toISOString()
|
||||
}));
|
||||
|
||||
|
||||
// 重新计算复合题的总分(所有子题目分数之和)
|
||||
const newTotalScore = (subQuestion.subQuestions || []).reduce((total, sq) => total + (sq.score || 0), 0);
|
||||
subQuestion.score = Math.round(newTotalScore * 10) / 10; // 保留一位小数
|
||||
|
||||
|
||||
console.log('复合题分数更新:', {
|
||||
subQuestionId: subQuestion.id,
|
||||
subQuestions: (subQuestion.subQuestions || []).map(sq => ({ title: sq.title, score: sq.score })),
|
||||
@ -796,15 +936,15 @@ const handleBatchScoreConfirm = (updatedQuestions: BigQuestion[]) => {
|
||||
// 如果在BatchSetScoreModal中没有单独设置小题分数,就平均分配
|
||||
const totalSubQuestions = subQuestion.subQuestions.length;
|
||||
const newTotalScore = subQuestion.score;
|
||||
|
||||
|
||||
// 检查是否需要平均分配(当所有小题分数为0或总分不匹配时)
|
||||
const currentSubQuestionsTotal = subQuestion.subQuestions.reduce((total, sq) => total + (sq.score || 0), 0);
|
||||
const shouldRedistribute = Math.abs(currentSubQuestionsTotal - newTotalScore) > 0.01;
|
||||
|
||||
|
||||
if (shouldRedistribute && totalSubQuestions > 0) {
|
||||
// 平均分配分数到各个小题(保留一位小数)
|
||||
const averageScore = Math.round((newTotalScore / totalSubQuestions) * 10) / 10;
|
||||
|
||||
|
||||
subQuestion.subQuestions.forEach((sq, index) => {
|
||||
if (index === totalSubQuestions - 1) {
|
||||
// 最后一题承担余数,确保总分准确
|
||||
@ -813,7 +953,7 @@ const handleBatchScoreConfirm = (updatedQuestions: BigQuestion[]) => {
|
||||
sq.score = averageScore;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log('复合题分数重新分配:', {
|
||||
questionId: subQuestion.id,
|
||||
totalScore: newTotalScore,
|
||||
@ -847,25 +987,25 @@ const examSettingsData = computed(() => ({
|
||||
endTime: null,
|
||||
category: (examForm.type === 1 ? 'exam' : 'practice') as 'exam' | 'practice',
|
||||
timeLimit: 'limited' as 'unlimited' | 'limited' | 'no_limit',
|
||||
timeLimitValue: 0,
|
||||
timeLimitValue: '0',
|
||||
examTimes: 'unlimited' as 'unlimited' | 'limited' | 'each_day',
|
||||
examTimesValue: 1,
|
||||
examTimesValue: '1',
|
||||
dailyLimit: 1,
|
||||
chapter: '',
|
||||
passScore: examForm.passScore,
|
||||
passScore: examForm.passScore.toString(),
|
||||
participants: 'all' as 'all' | 'by_school',
|
||||
selectedClasses: [],
|
||||
maxParticipants: 0,
|
||||
maxParticipants: '0',
|
||||
instructions: examForm.instructions,
|
||||
|
||||
// 考试模式专用
|
||||
enforceOrder: false,
|
||||
enforceInstructions: false,
|
||||
readingTime: 10,
|
||||
readingTime: '10',
|
||||
submitSettings: {
|
||||
allowEarlySubmit: true,
|
||||
},
|
||||
gradingDelay: 60,
|
||||
gradingDelay: '60',
|
||||
scoreDisplay: 'show_all' as 'show_all' | 'show_score' | 'hide_all',
|
||||
detailedSettings: {
|
||||
showQuestions: false,
|
||||
@ -873,15 +1013,15 @@ const examSettingsData = computed(() => ({
|
||||
showSubmissionTime: false,
|
||||
},
|
||||
timerEnabled: false,
|
||||
timerDuration: examForm.duration,
|
||||
timerDuration: examForm.duration.toString(),
|
||||
answerType: 'auto_save' as 'auto_save' | 'manual_save' | 'multiple_submit',
|
||||
detailScoreMode: 'question' as 'question' | 'automatic' | 'show_current' | 'show_all',
|
||||
showRanking: false,
|
||||
courseProgress: 0,
|
||||
courseProgress: '0',
|
||||
|
||||
// 练习模式专用
|
||||
correctnessMode: 'no_limit' as 'no_limit' | 'limit_wrong',
|
||||
wrongLimit: 10,
|
||||
wrongLimit: '10',
|
||||
practiceSettings: {
|
||||
showCorrectAnswer: false,
|
||||
showWrongAnswer: false,
|
||||
@ -934,7 +1074,7 @@ const handleQuestionBankConfirm = (selectedQuestions: any[]) => {
|
||||
title: question.title,
|
||||
score: question.score,
|
||||
difficulty: question.difficulty,
|
||||
required: true,
|
||||
required: 'true',
|
||||
explanation: '', // 添加默认的解析字段
|
||||
textAnswer: '', // 添加默认的文本答案字段
|
||||
createTime: new Date().toISOString()
|
||||
@ -998,6 +1138,58 @@ const getQuestionTypeFromString = (typeString: string) => {
|
||||
return typeMap[typeString] || 'single_choice';
|
||||
};
|
||||
|
||||
// 清除试卷表单内容
|
||||
const clearExamForm = () => {
|
||||
// 检查是否有内容需要清除
|
||||
const hasContent = examForm.title.trim() ||
|
||||
examForm.description.trim() ||
|
||||
examForm.instructions.trim() ||
|
||||
examForm.questions.some((q: any) => q.title.trim() || (q.description && q.description.trim()) || q.subQuestions.length > 0);
|
||||
|
||||
if (!hasContent) {
|
||||
message.info('表单内容已为空,无需清除');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示确认对话框
|
||||
dialog.warning({
|
||||
title: '确认清除',
|
||||
content: '确定要清除所有试卷内容吗?此操作不可撤销!',
|
||||
positiveText: '确定清除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
// 重置试卷基本信息
|
||||
examForm.title = '';
|
||||
examForm.type = 1;
|
||||
examForm.description = '';
|
||||
examForm.totalScore = 0;
|
||||
examForm.duration = 60;
|
||||
examForm.passScore = 60;
|
||||
examForm.instructions = '';
|
||||
examForm.useAIGrading = false;
|
||||
|
||||
// 重置题目列表,只保留一个空的大题
|
||||
examForm.questions = [
|
||||
{
|
||||
id: '1',
|
||||
title: '',
|
||||
description: '',
|
||||
sort: 1,
|
||||
totalScore: 0,
|
||||
subQuestions: [],
|
||||
createTime: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
// 清除sessionStorage中的自动保存数据
|
||||
sessionStorage.removeItem('examPreviewData');
|
||||
|
||||
message.success('试卷内容已清除');
|
||||
console.log('✅ 试卷表单内容已清除');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 保存试卷
|
||||
const saveExam = async () => {
|
||||
// 验证数据
|
||||
@ -1037,30 +1229,88 @@ const saveExam = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 准备API数据
|
||||
// 准备API数据 - 匹配 /aiol/aiolPaper/add 接口
|
||||
const apiData = {
|
||||
name: examForm.title,
|
||||
category: examForm.type === 1 ? '考试' : '练习',
|
||||
description: examForm.description || '',
|
||||
title: examForm.title,
|
||||
generateMode: examForm.type === 1 ? 0 : 1, // 0: 固定试卷组, 1: 随机抽题组卷
|
||||
rules: '', // 组卷规则,暂时为空
|
||||
repoId: '', // 题库ID,暂时为空
|
||||
totalScore: examForm.totalScore,
|
||||
difficulty: getDifficultyLevel(examForm.totalScore),
|
||||
duration: examForm.duration,
|
||||
questions: formatQuestionsForAPI(examForm.questions)
|
||||
passScore: examForm.passScore || Math.floor(examForm.totalScore * 0.6), // 及格分
|
||||
requireReview: examForm.useAIGrading ? 1 : 0 // 是否需要批阅
|
||||
};
|
||||
|
||||
console.log('🚀 准备保存试卷数据:', apiData);
|
||||
|
||||
// 调用API创建试卷
|
||||
const response = await ExamApi.createExamPaper(apiData);
|
||||
console.log('✅ 创建试卷成功:', response);
|
||||
// 根据模式调用不同的API
|
||||
let response;
|
||||
let paperId: string;
|
||||
|
||||
if (isEditMode.value) {
|
||||
// 编辑模式:更新试卷
|
||||
paperId = examId.value;
|
||||
console.log('🚀 编辑模式:更新试卷,ID:', paperId);
|
||||
response = await ExamApi.updateExamPaper(examId.value, {
|
||||
title: examForm.title,
|
||||
generateMode: examForm.type === 1 ? 0 : 1, // 0: 固定试卷组, 1: 随机抽题组卷
|
||||
rules: '', // 组卷规则,暂时为空
|
||||
repoId: '', // 题库ID,暂时为空
|
||||
totalScore: examForm.totalScore,
|
||||
passScore: examForm.passScore || Math.floor(examForm.totalScore * 0.6),
|
||||
requireReview: examForm.useAIGrading ? 1 : 0,
|
||||
description: examForm.description,
|
||||
duration: examForm.duration,
|
||||
instructions: examForm.instructions,
|
||||
useAIGrading: examForm.useAIGrading
|
||||
});
|
||||
console.log('✅ 更新试卷成功:', response);
|
||||
} else {
|
||||
// 新建模式:创建试卷
|
||||
response = await ExamApi.createExamPaper(apiData);
|
||||
console.log('✅ 创建试卷成功:', response);
|
||||
|
||||
// 从响应中获取试卷ID
|
||||
if (response.data) {
|
||||
paperId = response.data;
|
||||
} else {
|
||||
throw new Error('创建试卷失败,无法获取试卷ID');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存题目数据
|
||||
if (paperId) {
|
||||
console.log('🚀 开始保存题目数据,试卷ID:', paperId);
|
||||
console.log('📊 当前题目数据:', examForm.questions);
|
||||
|
||||
// 检查是否有题目数据
|
||||
const hasQuestions = examForm.questions.some(bigQuestion =>
|
||||
bigQuestion.subQuestions && bigQuestion.subQuestions.length > 0
|
||||
);
|
||||
|
||||
if (hasQuestions) {
|
||||
console.log('📊 发现题目数据,开始保存');
|
||||
await saveExamQuestions(paperId);
|
||||
} else {
|
||||
console.log('⚠️ 没有题目数据,跳过题目保存');
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 无法获取试卷ID,跳过题目保存');
|
||||
}
|
||||
|
||||
dialog.success({
|
||||
title: '保存成功',
|
||||
content: '试卷保存成功!',
|
||||
content: isEditMode.value ? '试卷更新成功!' : '试卷保存成功!',
|
||||
positiveText: '确定',
|
||||
onPositiveClick: () => {
|
||||
// 保存成功后返回试卷列表页面
|
||||
router.back();
|
||||
if (isEditMode.value) {
|
||||
// 编辑模式:保存成功后重新加载数据
|
||||
loadExamDetail(examId.value);
|
||||
} else {
|
||||
// 新建模式:保存成功后清除表单内容
|
||||
clearExamForm();
|
||||
// 返回试卷列表页面
|
||||
router.back();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@ -1074,31 +1324,182 @@ const saveExam = async () => {
|
||||
}
|
||||
|
||||
// 根据总分计算难度等级
|
||||
const getDifficultyLevel = (totalScore: number): string => {
|
||||
if (totalScore <= 60) return 'easy';
|
||||
if (totalScore <= 100) return 'medium';
|
||||
return 'hard';
|
||||
}
|
||||
// 难度等级计算函数(暂时未使用)
|
||||
// const getDifficultyLevel = (totalScore: number): string => {
|
||||
// if (totalScore <= 60) return 'easy';
|
||||
// if (totalScore <= 100) return 'medium';
|
||||
// return 'hard';
|
||||
// }
|
||||
|
||||
// 将前端题目类型转换为API需要的数字类型
|
||||
const convertQuestionTypeToNumber = (type: QuestionType): number => {
|
||||
const typeMap: Record<QuestionType, number> = {
|
||||
[QuestionType.SINGLE_CHOICE]: 0, // 单选题
|
||||
[QuestionType.MULTIPLE_CHOICE]: 1, // 多选题
|
||||
[QuestionType.TRUE_FALSE]: 2, // 判断题
|
||||
[QuestionType.FILL_BLANK]: 3, // 填空题
|
||||
[QuestionType.SHORT_ANSWER]: 4, // 简答题
|
||||
[QuestionType.COMPOSITE]: 5 // 复合题
|
||||
};
|
||||
return typeMap[type] || 0;
|
||||
};
|
||||
|
||||
// 将API返回的数字类型转换为前端题目类型
|
||||
const convertNumberToQuestionType = (type: number): QuestionType => {
|
||||
const typeMap: Record<number, QuestionType> = {
|
||||
0: QuestionType.SINGLE_CHOICE, // 单选题
|
||||
1: QuestionType.MULTIPLE_CHOICE, // 多选题
|
||||
2: QuestionType.TRUE_FALSE, // 判断题
|
||||
3: QuestionType.FILL_BLANK, // 填空题
|
||||
4: QuestionType.SHORT_ANSWER, // 简答题
|
||||
5: QuestionType.COMPOSITE // 复合题
|
||||
};
|
||||
return typeMap[type] || QuestionType.SINGLE_CHOICE;
|
||||
};
|
||||
|
||||
// 保存试卷题目
|
||||
const saveExamQuestions = async (paperId: string) => {
|
||||
try {
|
||||
console.log('🚀 开始保存试卷题目:', paperId);
|
||||
|
||||
// 如果是编辑模式,先删除现有的题目关联
|
||||
if (isEditMode.value) {
|
||||
console.log('🗑️ 编辑模式:删除现有题目关联');
|
||||
// 这里可以添加删除现有题目关联的逻辑
|
||||
// 暂时跳过,因为API可能不支持批量删除
|
||||
}
|
||||
|
||||
let questionOrder = 1;
|
||||
|
||||
// 检查是否有题目数据
|
||||
if (!examForm.questions || examForm.questions.length === 0) {
|
||||
console.log('⚠️ 没有题目数据,跳过题目保存');
|
||||
return;
|
||||
}
|
||||
|
||||
// 遍历所有大题
|
||||
console.log('📊 开始遍历大题,总数:', examForm.questions.length);
|
||||
for (const bigQuestion of examForm.questions) {
|
||||
console.log('📊 处理大题:', bigQuestion.title, '小题数量:', bigQuestion.subQuestions?.length || 0);
|
||||
if (bigQuestion.subQuestions && bigQuestion.subQuestions.length > 0) {
|
||||
// 遍历大题下的所有小题
|
||||
for (const subQuestion of bigQuestion.subQuestions) {
|
||||
console.log('📊 处理小题:', subQuestion.title, '类型:', subQuestion.type);
|
||||
try {
|
||||
// 创建题目
|
||||
const questionData = {
|
||||
repoId: '1958492351656955905', // 使用从题库模态框获取的题库ID
|
||||
parentId: '', // 父题目ID,暂时为空
|
||||
type: convertQuestionTypeToNumber(subQuestion.type), // 题目类型
|
||||
content: subQuestion.title || '', // 题干
|
||||
analysis: '', // 题目解析,暂时为空
|
||||
difficulty: 1, // 难度,默认1
|
||||
score: subQuestion.score || 0, // 分值
|
||||
degree: 1, // 程度,默认1
|
||||
ability: 1 // 能力,默认1
|
||||
};
|
||||
|
||||
console.log('📝 创建题目:', questionData);
|
||||
const questionResponse = await ExamApi.createQuestion(questionData);
|
||||
|
||||
if (questionResponse.data) {
|
||||
// 从响应中提取题目ID
|
||||
let questionId;
|
||||
if (typeof questionResponse.data === 'string') {
|
||||
questionId = questionResponse.data;
|
||||
} else if (questionResponse.data && typeof questionResponse.data === 'object') {
|
||||
questionId = (questionResponse.data as any).result || questionResponse.data;
|
||||
} else {
|
||||
throw new Error('无法获取题目ID');
|
||||
}
|
||||
|
||||
console.log('✅ 题目创建成功,ID:', questionId);
|
||||
console.log('📊 题目响应详情:', questionResponse);
|
||||
|
||||
// 创建题目选项(如果是选择题)
|
||||
if (subQuestion.options && subQuestion.options.length > 0) {
|
||||
console.log('📊 题目选项数据:', subQuestion.options);
|
||||
for (let i = 0; i < subQuestion.options.length; i++) {
|
||||
const option = subQuestion.options[i];
|
||||
console.log('📊 处理选项:', option);
|
||||
|
||||
const optionData = {
|
||||
questionId: questionId,
|
||||
content: (option as any).text || option.content || '',
|
||||
izCorrent: (option as any).isCorrect ? 1 : 0,
|
||||
orderNo: i + 1
|
||||
};
|
||||
|
||||
console.log('📝 创建题目选项:', optionData);
|
||||
await ExamApi.createQuestionOption(optionData);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建题目答案(如果有答案)
|
||||
if (subQuestion.correctAnswer !== null && subQuestion.correctAnswer !== undefined) {
|
||||
console.log('📊 题目答案数据:', subQuestion.correctAnswer);
|
||||
const answerData = {
|
||||
questionId: questionId,
|
||||
answerText: String(subQuestion.correctAnswer),
|
||||
orderNo: 1
|
||||
};
|
||||
|
||||
console.log('📝 创建题目答案:', answerData);
|
||||
await ExamApi.createQuestionAnswer(answerData);
|
||||
} else {
|
||||
console.log('📝 题目无答案,跳过答案创建');
|
||||
}
|
||||
|
||||
// 将题目关联到试卷
|
||||
const paperQuestionData = {
|
||||
paperId: paperId,
|
||||
questionId: questionId,
|
||||
orderNo: questionOrder,
|
||||
score: subQuestion.score || 0
|
||||
};
|
||||
|
||||
console.log('📝 关联题目到试卷:', paperQuestionData);
|
||||
const paperQuestionResponse = await ExamApi.addExamPaperQuestion(paperQuestionData);
|
||||
console.log('✅ 题目关联成功:', paperQuestionResponse);
|
||||
|
||||
questionOrder++;
|
||||
} else {
|
||||
console.error('❌ 题目创建失败:', questionResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 保存小题失败:', subQuestion, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ 试卷题目保存完成,共处理', questionOrder - 1, '道题目');
|
||||
} catch (error) {
|
||||
console.error('❌ 保存试卷题目失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化题目数据为API需要的格式
|
||||
const formatQuestionsForAPI = (questions: any[]): any[] => {
|
||||
return questions.map((bigQuestion, index) => ({
|
||||
id: bigQuestion.id,
|
||||
title: bigQuestion.title,
|
||||
description: bigQuestion.description,
|
||||
sort: index + 1,
|
||||
totalScore: bigQuestion.totalScore,
|
||||
subQuestions: bigQuestion.subQuestions.map((subQuestion: any, subIndex: number) => ({
|
||||
id: subQuestion.id,
|
||||
title: subQuestion.title,
|
||||
type: subQuestion.type,
|
||||
options: subQuestion.options || [],
|
||||
correctAnswer: subQuestion.correctAnswer,
|
||||
score: subQuestion.score,
|
||||
sort: subIndex + 1
|
||||
}))
|
||||
}));
|
||||
}
|
||||
// 格式化题目数据函数(暂时未使用)
|
||||
// const formatQuestionsForAPI = (questions: any[]): any[] => {
|
||||
// return questions.map((bigQuestion, index) => ({
|
||||
// id: bigQuestion.id,
|
||||
// title: bigQuestion.title,
|
||||
// description: bigQuestion.description,
|
||||
// sort: index + 1,
|
||||
// totalScore: bigQuestion.totalScore,
|
||||
// subQuestions: bigQuestion.subQuestions.map((subQuestion: any, subIndex: number) => ({
|
||||
// id: subQuestion.id,
|
||||
// title: subQuestion.title,
|
||||
// type: subQuestion.type,
|
||||
// options: subQuestion.options || [],
|
||||
// correctAnswer: subQuestion.correctAnswer,
|
||||
// score: subQuestion.score,
|
||||
// sort: subIndex + 1
|
||||
// }))
|
||||
// }));
|
||||
// }
|
||||
|
||||
// 预览试卷
|
||||
const previewExam = () => {
|
||||
@ -1179,7 +1580,15 @@ const restoreExamData = () => {
|
||||
|
||||
// 页面挂载时尝试恢复数据
|
||||
onMounted(() => {
|
||||
restoreExamData();
|
||||
// 检查是否是编辑模式
|
||||
if (route.params.id) {
|
||||
isEditMode.value = true;
|
||||
examId.value = route.params.id as string;
|
||||
loadExamDetail(examId.value);
|
||||
} else {
|
||||
// 新建模式:恢复自动保存的数据
|
||||
restoreExamData();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面卸载时清理定时器
|
||||
|
@ -46,8 +46,7 @@
|
||||
v-model:trueFalseAnswer="questionForm.trueFalseAnswer"
|
||||
v-model:fillBlankAnswers="questionForm.fillBlankAnswers"
|
||||
v-model:shortAnswer="questionForm.shortAnswer"
|
||||
v-model:compositeData="questionForm.compositeData"
|
||||
v-model:title="questionForm.title"
|
||||
v-model:compositeData="questionForm.compositeData" v-model:title="questionForm.title"
|
||||
v-model:explanation="questionForm.explanation" />
|
||||
|
||||
<!-- 基本信息 -->
|
||||
@ -123,7 +122,7 @@
|
||||
|
||||
<!-- 填空题答案 -->
|
||||
<div v-if="questionForm.type === 'fill_blank'" class="preview-fill-blanks">
|
||||
<div v-for="(answer, index) in questionForm.fillBlankAnswers" :key="index"
|
||||
<div v-for="(answer, index) in questionForm.fillBlankAnswers" :key="index"
|
||||
class="preview-fill-blank-item">
|
||||
<div class="blank-number">{{ index + 1 }}.</div>
|
||||
<div class="blank-content">
|
||||
@ -148,51 +147,54 @@
|
||||
<span class="empty-text">请添加小题</span>
|
||||
</div>
|
||||
<div v-else class="composite-sub-questions">
|
||||
<div v-for="(subQuestion, index) in questionForm.compositeData.subQuestions"
|
||||
:key="subQuestion.id"
|
||||
class="composite-sub-question">
|
||||
<div v-for="(subQuestion, index) in questionForm.compositeData.subQuestions"
|
||||
:key="subQuestion.id" class="composite-sub-question">
|
||||
<div class="sub-question-header">
|
||||
<span class="sub-question-number">{{ index + 1 }}.</span>
|
||||
<span class="sub-question-title">{{ subQuestion.title || '请输入小题内容...' }}</span>
|
||||
<span class="sub-question-type">[{{ getQuestionTypeLabel(subQuestion.type) }}]</span>
|
||||
<span class="sub-question-type">[{{ getQuestionTypeLabel(subQuestion.type)
|
||||
}}]</span>
|
||||
<span class="sub-question-score">({{ subQuestion.score }}分)</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 小题选项预览 -->
|
||||
<div v-if="subQuestion.type === 'single' && subQuestion.data" class="sub-question-options">
|
||||
<div v-if="subQuestion.type === 'single' && subQuestion.data"
|
||||
class="sub-question-options">
|
||||
<div v-for="(option, optIndex) in subQuestion.data" :key="optIndex"
|
||||
class="preview-option-item"
|
||||
:class="{ 'correct-answer': subQuestion.correctAnswer === optIndex }">
|
||||
class="preview-option-item"
|
||||
:class="{ 'correct-answer': subQuestion.correctAnswer === optIndex }">
|
||||
<span class="option-letter">{{ String.fromCharCode(65 + optIndex) }}</span>
|
||||
<span class="option-content">{{ option.option || '请输入内容' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="subQuestion.type === 'multiple' && subQuestion.data" class="sub-question-options">
|
||||
|
||||
<div v-else-if="subQuestion.type === 'multiple' && subQuestion.data"
|
||||
class="sub-question-options">
|
||||
<div v-for="(option, optIndex) in subQuestion.data" :key="optIndex"
|
||||
class="preview-option-item"
|
||||
:class="{ 'correct-answer': subQuestion.correctAnswers && subQuestion.correctAnswers.includes(optIndex) }">
|
||||
class="preview-option-item"
|
||||
:class="{ 'correct-answer': subQuestion.correctAnswers && subQuestion.correctAnswers.includes(optIndex) }">
|
||||
<span class="option-letter">{{ String.fromCharCode(65 + optIndex) }}</span>
|
||||
<span class="option-content">{{ option.option || '请输入内容' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-else-if="subQuestion.type === 'truefalse'" class="sub-question-options">
|
||||
<div class="preview-option-item"
|
||||
:class="{ 'correct-answer': subQuestion.answer === true }">
|
||||
:class="{ 'correct-answer': subQuestion.answer === true }">
|
||||
<span class="option-letter">A</span>
|
||||
<span class="option-content">对</span>
|
||||
</div>
|
||||
<div class="preview-option-item"
|
||||
:class="{ 'correct-answer': subQuestion.answer === false }">
|
||||
:class="{ 'correct-answer': subQuestion.answer === false }">
|
||||
<span class="option-letter">B</span>
|
||||
<span class="option-content">错</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="subQuestion.type === 'fillblank' && subQuestion.answers" class="sub-question-fill-blanks">
|
||||
<div v-for="(answer, answerIndex) in subQuestion.answers" :key="answerIndex"
|
||||
class="preview-fill-blank-item">
|
||||
|
||||
<div v-else-if="subQuestion.type === 'fillblank' && subQuestion.answers"
|
||||
class="sub-question-fill-blanks">
|
||||
<div v-for="(answer, answerIndex) in subQuestion.answers" :key="answerIndex"
|
||||
class="preview-fill-blank-item">
|
||||
<div class="blank-number">{{ answerIndex + 1 }}.</div>
|
||||
<div class="blank-content">
|
||||
<span class="blank-answer">{{ answer.value || '请输入答案' }}</span>
|
||||
@ -201,14 +203,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="subQuestion.type === 'shortanswer'" class="sub-question-short-answer">
|
||||
|
||||
<div v-else-if="subQuestion.type === 'shortanswer'"
|
||||
class="sub-question-short-answer">
|
||||
<div class="short-answer-label">参考答案:</div>
|
||||
<div class="short-answer-content">
|
||||
{{ subQuestion.data || '请输入参考答案' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 小题解析 -->
|
||||
<div v-if="subQuestion.explanation" class="sub-question-explanation">
|
||||
<div class="explanation-label">解析:</div>
|
||||
@ -291,27 +294,18 @@ const questionTypeOptions = ref([
|
||||
{ label: '复合题', value: 'composite' }
|
||||
]);
|
||||
|
||||
// 分类选项(从试题管理页面同步)
|
||||
const categoryOptions = ref([
|
||||
{ label: '分类试题', value: 'category' },
|
||||
{ label: '考试试题', value: 'exam' },
|
||||
{ label: '练习试题', value: 'practice' },
|
||||
{ label: '模拟试题', value: 'simulation' }
|
||||
]);
|
||||
// 分类选项(从API获取)
|
||||
const categoryOptions = ref<Array<{ label: string, value: string }>>([]);
|
||||
|
||||
// 难度选项
|
||||
const difficultyOptions = ref([
|
||||
{ label: '简单', value: 0 },
|
||||
{ label: '中等', value: 1 },
|
||||
{ label: '困难', value: 2 }
|
||||
]);
|
||||
// 难度选项(从API获取)
|
||||
const difficultyOptions = ref<Array<{ label: string, value: string }>>([]);
|
||||
|
||||
// 试题表单数据
|
||||
const questionForm = reactive({
|
||||
type: 'single_choice', // 默认单选题
|
||||
category: '',
|
||||
difficulty: 0, // 默认简单
|
||||
score: 10,
|
||||
difficulty: '0', // 默认简单(字符串类型)
|
||||
score: '10', // 改为字符串类型
|
||||
title: '',
|
||||
options: [
|
||||
{ content: '' },
|
||||
@ -322,7 +316,7 @@ const questionForm = reactive({
|
||||
correctAnswer: null as number | null, // 单选题正确答案索引
|
||||
correctAnswers: [] as number[], // 多选题正确答案索引数组
|
||||
trueFalseAnswer: null as boolean | null, // 判断题答案
|
||||
fillBlankAnswers: [{ content: '', score: 1, caseSensitive: false }] as Array<{content: string, score: number, caseSensitive: boolean}>, // 填空题答案
|
||||
fillBlankAnswers: [{ content: '', score: 1, caseSensitive: false }] as Array<{ content: string, score: number, caseSensitive: boolean }>, // 填空题答案
|
||||
shortAnswer: '', // 简答题答案
|
||||
compositeData: { subQuestions: [] } as { subQuestions: any[] }, // 复合题数据
|
||||
explanation: '' // 解析
|
||||
@ -346,15 +340,23 @@ const formRules = {
|
||||
},
|
||||
difficulty: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择难度',
|
||||
trigger: 'change'
|
||||
},
|
||||
score: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请输入分值',
|
||||
trigger: 'blur'
|
||||
trigger: 'blur',
|
||||
validator: (_rule: any, value: any) => {
|
||||
if (!value || value === '') {
|
||||
return new Error('请输入分值');
|
||||
}
|
||||
const numValue = Number(value);
|
||||
if (isNaN(numValue) || numValue <= 0) {
|
||||
return new Error('分值必须是大于0的数字');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
title: {
|
||||
required: true,
|
||||
@ -426,7 +428,7 @@ const getQuestionTypeNumber = (type: string): number => {
|
||||
return typeMap[type] || 0;
|
||||
};
|
||||
|
||||
// 难度映射函数:将数字难度转换为数字(保持原值)
|
||||
// 难度映射函数:将字符串难度转换为数字
|
||||
const getDifficultyNumber = (difficulty: number | string): number => {
|
||||
// 如果已经是数字,直接返回
|
||||
if (typeof difficulty === 'number') {
|
||||
@ -435,9 +437,12 @@ const getDifficultyNumber = (difficulty: number | string): number => {
|
||||
|
||||
// 如果是字符串,进行转换
|
||||
const difficultyMap: Record<string, number> = {
|
||||
'easy': 0, // 简单
|
||||
'medium': 1, // 中等
|
||||
'hard': 2 // 困难
|
||||
'0': 0, // 简单
|
||||
'1': 1, // 中等
|
||||
'2': 2, // 困难
|
||||
'easy': 0, // 简单(兼容旧格式)
|
||||
'medium': 1, // 中等(兼容旧格式)
|
||||
'hard': 2 // 困难(兼容旧格式)
|
||||
};
|
||||
return difficultyMap[difficulty] || 0;
|
||||
};
|
||||
@ -445,6 +450,20 @@ const getDifficultyNumber = (difficulty: number | string): number => {
|
||||
// 保存试题
|
||||
const saveQuestion = async () => {
|
||||
try {
|
||||
console.log('🔍 开始保存试题,当前表单数据:', {
|
||||
type: questionForm.type,
|
||||
title: questionForm.title,
|
||||
category: questionForm.category,
|
||||
difficulty: questionForm.difficulty,
|
||||
score: Number(questionForm.score),
|
||||
options: questionForm.options,
|
||||
correctAnswer: questionForm.correctAnswer,
|
||||
correctAnswers: questionForm.correctAnswers,
|
||||
trueFalseAnswer: questionForm.trueFalseAnswer,
|
||||
fillBlankAnswers: questionForm.fillBlankAnswers,
|
||||
shortAnswer: questionForm.shortAnswer
|
||||
});
|
||||
|
||||
// 表单验证
|
||||
await formRef.value?.validate();
|
||||
|
||||
@ -458,7 +477,7 @@ const saveQuestion = async () => {
|
||||
// 获取题库ID(可能从路由参数或者查询参数获取)
|
||||
// 如果从题库管理页面跳转过来,应该有bankId或者通过其他方式传递
|
||||
let bankId = route.params.bankId as string || route.params.id as string || route.query.bankId as string;
|
||||
|
||||
|
||||
if (!bankId) {
|
||||
// 尝试从浏览器历史记录或者本地存储获取
|
||||
const referrer = document.referrer;
|
||||
@ -467,7 +486,7 @@ const saveQuestion = async () => {
|
||||
bankId = bankIdMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!bankId) {
|
||||
message.error('缺少题库ID参数,请从题库管理页面进入');
|
||||
return;
|
||||
@ -509,10 +528,97 @@ const saveQuestion = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 准备题目选项数据
|
||||
const prepareQuestionOptions = () => {
|
||||
const questionType = getQuestionTypeNumber(questionForm.type);
|
||||
const options: Array<{
|
||||
content: string;
|
||||
isCorrect: boolean;
|
||||
orderNo: number;
|
||||
}> = [];
|
||||
|
||||
if (questionType === 0) { // 单选题
|
||||
const formOptions = questionForm.options || [];
|
||||
const correctAnswer = questionForm.correctAnswer;
|
||||
|
||||
console.log('🔍 准备单选题选项 - 表单选项:', formOptions);
|
||||
console.log('🔍 准备单选题选项 - 正确答案索引:', correctAnswer);
|
||||
|
||||
formOptions.forEach((option: any, index: number) => {
|
||||
const content = option.content || option.text || '';
|
||||
console.log(`🔍 处理选项 ${index}:`, { content, isCorrect: index === correctAnswer });
|
||||
|
||||
// 只处理有内容的选项
|
||||
if (content.trim()) {
|
||||
options.push({
|
||||
content: content,
|
||||
isCorrect: index === correctAnswer, // 使用索引比较
|
||||
orderNo: index
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (questionType === 1) { // 多选题
|
||||
const formOptions = questionForm.options || [];
|
||||
const correctAnswers = questionForm.correctAnswers || [];
|
||||
|
||||
formOptions.forEach((option: any, index: number) => {
|
||||
const content = option.content || option.text || '';
|
||||
// 只处理有内容的选项
|
||||
if (content.trim()) {
|
||||
options.push({
|
||||
content: content,
|
||||
isCorrect: correctAnswers.includes(index), // 使用索引比较
|
||||
orderNo: index
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (questionType === 2) { // 判断题
|
||||
options.push(
|
||||
{ content: '正确', isCorrect: questionForm.trueFalseAnswer === true, orderNo: 0 },
|
||||
{ content: '错误', isCorrect: questionForm.trueFalseAnswer === false, orderNo: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('🔍 准备题目选项数据结果:', options);
|
||||
return options;
|
||||
};
|
||||
|
||||
// 准备题目答案数据
|
||||
const prepareQuestionAnswers = () => {
|
||||
const questionType = getQuestionTypeNumber(questionForm.type);
|
||||
const answers: Array<{
|
||||
answerText: string;
|
||||
orderNo: number;
|
||||
}> = [];
|
||||
|
||||
if (questionType === 3) { // 填空题
|
||||
const fillBlankAnswers = questionForm.fillBlankAnswers || [];
|
||||
fillBlankAnswers.forEach((answer: any, index: number) => {
|
||||
const answerText = typeof answer === 'string' ? answer : answer.content;
|
||||
if (answerText && answerText.trim()) {
|
||||
answers.push({
|
||||
answerText: answerText.trim(),
|
||||
orderNo: index
|
||||
});
|
||||
}
|
||||
});
|
||||
} else if (questionType === 4) { // 简答题
|
||||
const shortAnswer = questionForm.shortAnswer;
|
||||
if (shortAnswer && shortAnswer.trim()) {
|
||||
answers.push({
|
||||
answerText: shortAnswer.trim(),
|
||||
orderNo: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return answers;
|
||||
};
|
||||
|
||||
// 创建新题目的完整流程
|
||||
const createNewQuestion = async (bankId: string) => {
|
||||
try {
|
||||
// 只调用一次API创建题目
|
||||
// 准备题目数据
|
||||
const questionData = {
|
||||
repoId: bankId, // 题库ID
|
||||
parentId: undefined, // 父题目ID,普通题目为undefined
|
||||
@ -520,34 +626,18 @@ const createNewQuestion = async (bankId: string) => {
|
||||
content: questionForm.title,
|
||||
analysis: questionForm.explanation || '',
|
||||
difficulty: getDifficultyNumber(questionForm.difficulty),
|
||||
score: questionForm.score,
|
||||
score: Number(questionForm.score),
|
||||
degree: 1, // 程度,默认为1
|
||||
ability: 1 // 能力,默认为1
|
||||
ability: 1, // 能力,默认为1
|
||||
options: prepareQuestionOptions(),
|
||||
answers: prepareQuestionAnswers()
|
||||
};
|
||||
|
||||
console.log('🚀 创建题目,数据:', questionData);
|
||||
const response = await ExamApi.createQuestion(questionData);
|
||||
console.log('📊 创建题目API响应:', response);
|
||||
console.log('🚀 创建完整题目,数据:', questionData);
|
||||
const response = await ExamApi.createCompleteQuestion(questionData);
|
||||
console.log('📊 创建完整题目API响应:', response);
|
||||
|
||||
// 处理API响应
|
||||
let success = false;
|
||||
let questionId = null;
|
||||
|
||||
if (response.data) {
|
||||
const apiResponse = response.data as any;
|
||||
|
||||
// 检查是否是包装格式 {success, code, result}
|
||||
if (typeof apiResponse === 'object' && ('success' in apiResponse || 'code' in apiResponse)) {
|
||||
success = apiResponse.success === true || apiResponse.code === 200 || apiResponse.code === 0;
|
||||
questionId = apiResponse.result || apiResponse.data;
|
||||
} else {
|
||||
// 直接是题目ID
|
||||
success = true;
|
||||
questionId = apiResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success || !questionId) {
|
||||
if (!response.data) {
|
||||
throw new Error('创建题目失败:未获取到题目ID');
|
||||
}
|
||||
|
||||
@ -557,10 +647,10 @@ const createNewQuestion = async (bankId: string) => {
|
||||
const questionType = getQuestionTypeNumber(questionForm.type);
|
||||
if (questionType === 0 || questionType === 1 || questionType === 2) {
|
||||
// 单选、多选、判断题:创建选项
|
||||
await createQuestionOptions(questionId, questionType);
|
||||
await updateQuestionOptions(questionId!, questionType);
|
||||
} else if (questionType === 3 || questionType === 4) {
|
||||
// 填空题、简答题:创建答案
|
||||
await createQuestionAnswers(questionId, questionType);
|
||||
await createQuestionAnswers(questionId!, questionType);
|
||||
}
|
||||
|
||||
message.success('题目创建成功!');
|
||||
@ -573,98 +663,6 @@ const createNewQuestion = async (bankId: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 创建题目选项
|
||||
const createQuestionOptions = async (questionId: string, questionType: number) => {
|
||||
try {
|
||||
console.log('🚀 开始创建题目选项,题目ID:', questionId, '题目类型:', questionType);
|
||||
|
||||
let optionsToCreate: Array<{
|
||||
content: string;
|
||||
izCorrent: number;
|
||||
orderNo: number;
|
||||
}> = [];
|
||||
|
||||
if (questionType === 0) { // 单选题
|
||||
// 获取选项数据
|
||||
const options = questionForm.options || [];
|
||||
const correctAnswer = questionForm.correctAnswer;
|
||||
|
||||
console.log('🔍 单选题选项创建逻辑:', {
|
||||
options,
|
||||
correctAnswer,
|
||||
correctAnswerType: typeof correctAnswer
|
||||
});
|
||||
|
||||
optionsToCreate = options.map((option: any, index: number) => ({
|
||||
content: option.content || option.text || '',
|
||||
izCorrent: index === correctAnswer ? 1 : 0,
|
||||
orderNo: index
|
||||
}));
|
||||
|
||||
} else if (questionType === 1) { // 多选题
|
||||
const options = questionForm.options || [];
|
||||
const correctAnswers = questionForm.correctAnswers || [];
|
||||
|
||||
console.log('🔍 多选题选项创建逻辑:', {
|
||||
options,
|
||||
correctAnswers,
|
||||
correctAnswersType: typeof correctAnswers
|
||||
});
|
||||
|
||||
optionsToCreate = options.map((option: any, index: number) => ({
|
||||
content: option.content || option.text || '',
|
||||
izCorrent: correctAnswers.includes(index) ? 1 : 0,
|
||||
orderNo: index
|
||||
}));
|
||||
|
||||
} else if (questionType === 2) { // 判断题
|
||||
// 判断题固定两个选项:正确和错误
|
||||
const trueFalseAnswer = questionForm.trueFalseAnswer;
|
||||
const isCorrectTrue = trueFalseAnswer === true;
|
||||
|
||||
console.log('🔍 判断题选项创建逻辑:', {
|
||||
trueFalseAnswer,
|
||||
isCorrectTrue,
|
||||
trueFalseAnswerType: typeof trueFalseAnswer
|
||||
});
|
||||
|
||||
optionsToCreate = [
|
||||
{
|
||||
content: '正确',
|
||||
izCorrent: isCorrectTrue ? 1 : 0,
|
||||
orderNo: 0
|
||||
},
|
||||
{
|
||||
content: '错误',
|
||||
izCorrent: isCorrectTrue ? 0 : 1,
|
||||
orderNo: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
console.log('📊 准备创建的选项:', optionsToCreate);
|
||||
|
||||
// 批量创建选项
|
||||
for (const option of optionsToCreate) {
|
||||
const optionData = {
|
||||
questionId: questionId,
|
||||
content: option.content,
|
||||
izCorrent: option.izCorrent,
|
||||
orderNo: option.orderNo
|
||||
};
|
||||
|
||||
console.log('🚀 创建选项:', optionData);
|
||||
const optionResponse = await ExamApi.createQuestionOption(optionData);
|
||||
console.log('✅ 选项创建成功:', optionResponse);
|
||||
}
|
||||
|
||||
console.log('✅ 所有选项创建完成');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 创建题目选项失败:', error);
|
||||
throw new Error('创建题目选项失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 创建题目答案(填空题和简答题)
|
||||
const createQuestionAnswers = async (questionId: string, questionType: number) => {
|
||||
@ -729,7 +727,7 @@ const updateExistingQuestion = async (questionId: string) => {
|
||||
content: questionForm.title,
|
||||
analysis: questionForm.explanation || '',
|
||||
difficulty: getDifficultyNumber(questionForm.difficulty),
|
||||
score: questionForm.score
|
||||
score: Number(questionForm.score)
|
||||
};
|
||||
|
||||
console.log('🚀 更新题目基本信息:', questionData);
|
||||
@ -900,7 +898,7 @@ const validateAnswers = (): boolean => {
|
||||
message.error('请填写题目内容');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
switch (questionForm.type) {
|
||||
case 'single_choice':
|
||||
// 检查选项内容
|
||||
@ -959,30 +957,30 @@ const validateAnswers = (): boolean => {
|
||||
// 验证复合题设置
|
||||
const validateCompositeQuestion = (): boolean => {
|
||||
const compositeData = questionForm.compositeData;
|
||||
|
||||
|
||||
// 检查是否有小题
|
||||
if (!compositeData.subQuestions || compositeData.subQuestions.length === 0) {
|
||||
message.error('复合题至少需要添加一道小题');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 逐个验证小题
|
||||
for (let i = 0; i < compositeData.subQuestions.length; i++) {
|
||||
const subQuestion = compositeData.subQuestions[i];
|
||||
const subIndex = i + 1;
|
||||
|
||||
|
||||
// 检查小题题型是否选择
|
||||
if (!subQuestion.type) {
|
||||
message.error(`第${subIndex}小题请选择题型`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查小题标题
|
||||
if (!subQuestion.title || !subQuestion.title.trim()) {
|
||||
message.error(`第${subIndex}小题请输入题目内容`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 根据小题题型验证答案
|
||||
switch (subQuestion.type) {
|
||||
case 'single':
|
||||
@ -990,7 +988,7 @@ const validateCompositeQuestion = (): boolean => {
|
||||
message.error(`第${subIndex}小题(单选题)至少需要2个选项`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查选项内容
|
||||
let validOptionsCount = 0;
|
||||
for (let j = 0; j < subQuestion.data.length; j++) {
|
||||
@ -998,31 +996,31 @@ const validateCompositeQuestion = (): boolean => {
|
||||
validOptionsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (validOptionsCount < 2) {
|
||||
message.error(`第${subIndex}小题(单选题)至少需要2个有效选项`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查是否设置了正确答案
|
||||
if (subQuestion.correctAnswer === null || subQuestion.correctAnswer === undefined) {
|
||||
message.error(`第${subIndex}小题(单选题)请设置正确答案`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查正确答案索引是否有效
|
||||
if (subQuestion.correctAnswer >= subQuestion.data.length || subQuestion.correctAnswer < 0) {
|
||||
message.error(`第${subIndex}小题(单选题)正确答案设置错误`);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'multiple':
|
||||
if (!subQuestion.data || subQuestion.data.length < 2) {
|
||||
message.error(`第${subIndex}小题(多选题)至少需要2个选项`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查选项内容
|
||||
let validMultipleOptionsCount = 0;
|
||||
for (let j = 0; j < subQuestion.data.length; j++) {
|
||||
@ -1030,18 +1028,18 @@ const validateCompositeQuestion = (): boolean => {
|
||||
validMultipleOptionsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (validMultipleOptionsCount < 2) {
|
||||
message.error(`第${subIndex}小题(多选题)至少需要2个有效选项`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查是否设置了正确答案
|
||||
if (!subQuestion.correctAnswers || subQuestion.correctAnswers.length === 0) {
|
||||
message.error(`第${subIndex}小题(多选题)请设置正确答案`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查正确答案索引是否有效
|
||||
for (let j = 0; j < subQuestion.correctAnswers.length; j++) {
|
||||
if (subQuestion.correctAnswers[j] >= subQuestion.data.length || subQuestion.correctAnswers[j] < 0) {
|
||||
@ -1050,20 +1048,20 @@ const validateCompositeQuestion = (): boolean => {
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'truefalse':
|
||||
if (!subQuestion.answer) {
|
||||
message.error(`第${subIndex}小题(判断题)请设置正确答案`);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'fillblank':
|
||||
if (!subQuestion.answers || subQuestion.answers.length === 0) {
|
||||
message.error(`第${subIndex}小题(填空题)请至少添加一个答案`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查每个填空答案
|
||||
for (let j = 0; j < subQuestion.answers.length; j++) {
|
||||
if (!subQuestion.answers[j].value || !subQuestion.answers[j].value.trim()) {
|
||||
@ -1072,35 +1070,88 @@ const validateCompositeQuestion = (): boolean => {
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
case 'shortanswer':
|
||||
if (!subQuestion.data || !subQuestion.data.trim()) {
|
||||
message.error(`第${subIndex}小题(简答题)请设置参考答案`);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
message.error(`第${subIndex}小题题型设置错误`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 检查分值设置
|
||||
if (!subQuestion.score || subQuestion.score <= 0) {
|
||||
message.error(`第${subIndex}小题请设置有效的分值`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 加载分类和难度数据
|
||||
const loadCategoriesAndDifficulties = async () => {
|
||||
try {
|
||||
console.log('🚀 加载分类和难度数据');
|
||||
|
||||
// 并行加载分类和难度数据
|
||||
const [categoriesResponse, difficultiesResponse] = await Promise.all([
|
||||
ExamApi.getQuestionCategories(),
|
||||
ExamApi.getQuestionDifficulties()
|
||||
]);
|
||||
|
||||
// 处理分类数据
|
||||
if (categoriesResponse.data) {
|
||||
categoryOptions.value = categoriesResponse.data.map(item => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}));
|
||||
console.log('✅ 分类数据加载成功:', categoryOptions.value);
|
||||
}
|
||||
|
||||
// 处理难度数据
|
||||
if (difficultiesResponse.data) {
|
||||
difficultyOptions.value = difficultiesResponse.data.map(item => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
}));
|
||||
console.log('✅ 难度数据加载成功:', difficultyOptions.value);
|
||||
|
||||
// 如果当前难度值为空,设置默认值
|
||||
if (!questionForm.difficulty && difficultyOptions.value.length > 0) {
|
||||
questionForm.difficulty = difficultyOptions.value[0].value;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 加载分类和难度数据失败:', error);
|
||||
// 使用默认数据作为备选
|
||||
categoryOptions.value = [
|
||||
{ label: '考试试题', value: 'exam' },
|
||||
{ label: '练习试题', value: 'practice' },
|
||||
{ label: '模拟试题', value: 'simulation' }
|
||||
];
|
||||
difficultyOptions.value = [
|
||||
{ label: '简单', value: '0' },
|
||||
{ label: '中等', value: '1' },
|
||||
{ label: '困难', value: '2' }
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载
|
||||
onMounted(async () => {
|
||||
console.log('AddQuestion 组件挂载完成');
|
||||
console.log('编辑模式:', isEditMode.value);
|
||||
console.log('题目ID:', questionId);
|
||||
|
||||
// 加载分类和难度数据
|
||||
await loadCategoriesAndDifficulties();
|
||||
|
||||
// 如果是编辑模式,加载题目数据
|
||||
if (isEditMode.value && questionId) {
|
||||
// 优先从路由参数中获取数据
|
||||
@ -1127,15 +1178,34 @@ const renderQuestionData = (questionData: any) => {
|
||||
|
||||
if (!questionData) return;
|
||||
|
||||
const { question, answer = [], children = [] } = questionData;
|
||||
// 处理不同的数据结构
|
||||
let question = null;
|
||||
let answer = [];
|
||||
let children = [];
|
||||
|
||||
// 检查数据结构
|
||||
if (questionData.question) {
|
||||
// 格式1: { question, answer, children }
|
||||
question = questionData.question;
|
||||
answer = questionData.answer || [];
|
||||
children = questionData.children || [];
|
||||
} else if (questionData.id && questionData.type !== undefined) {
|
||||
// 格式2: 直接是题目对象
|
||||
question = questionData;
|
||||
answer = questionData.options || questionData.answer || [];
|
||||
children = questionData.children || [];
|
||||
} else {
|
||||
console.error('❌ 未知的数据结构:', questionData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (question) {
|
||||
// 设置基本信息
|
||||
questionForm.type = getQuestionTypeKey(question.type);
|
||||
questionForm.title = question.content || '';
|
||||
questionForm.explanation = question.analysis || '';
|
||||
questionForm.score = question.score || 10;
|
||||
questionForm.difficulty = question.difficulty || 0;
|
||||
questionForm.score = String(question.score || 10);
|
||||
questionForm.difficulty = String(question.difficulty || 0);
|
||||
|
||||
console.log('📝 题目基本信息:', {
|
||||
type: questionForm.type,
|
||||
@ -1144,6 +1214,8 @@ const renderQuestionData = (questionData: any) => {
|
||||
score: questionForm.score
|
||||
});
|
||||
|
||||
console.log('🔍 选项数据:', answer);
|
||||
|
||||
// 根据题目类型处理选项和答案
|
||||
if (question.type === 0) { // 单选题
|
||||
renderSingleChoiceData(answer);
|
||||
@ -1308,16 +1380,21 @@ const loadQuestionData = async (id: string) => {
|
||||
try {
|
||||
console.log('🚀 开始加载题目数据,题目ID:', id);
|
||||
|
||||
// 调用题目详情接口
|
||||
const response = await ExamApi.getQuestionDetail(id);
|
||||
console.log('📊 题目详情API响应:', response);
|
||||
// 并行获取题目详情和选项数据
|
||||
const [questionResponse, optionsResponse] = await Promise.all([
|
||||
ExamApi.getQuestionDetail(id),
|
||||
ExamApi.getQuestionOptions(id)
|
||||
]);
|
||||
|
||||
// 处理API响应
|
||||
console.log('📊 题目详情API响应:', questionResponse);
|
||||
console.log('📊 题目选项API响应:', optionsResponse);
|
||||
|
||||
// 处理题目详情响应
|
||||
let questionData = null;
|
||||
let success = false;
|
||||
|
||||
if (response.data) {
|
||||
const apiResponse = response.data as any;
|
||||
if (questionResponse.data) {
|
||||
const apiResponse = questionResponse.data as any;
|
||||
|
||||
// 检查是否是包装格式 {success, code, result}
|
||||
if (typeof apiResponse === 'object' && 'result' in apiResponse) {
|
||||
@ -1330,9 +1407,25 @@ const loadQuestionData = async (id: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理选项数据
|
||||
let optionsData = [];
|
||||
if (optionsResponse.data) {
|
||||
const apiResponse = optionsResponse.data as any;
|
||||
if (Array.isArray(apiResponse)) {
|
||||
optionsData = apiResponse;
|
||||
} else if (apiResponse && Array.isArray(apiResponse.result)) {
|
||||
optionsData = apiResponse.result;
|
||||
}
|
||||
}
|
||||
|
||||
if (success && questionData) {
|
||||
console.log('✅ 获取题目详情成功,开始渲染数据');
|
||||
renderQuestionData(questionData);
|
||||
// 将选项数据添加到题目数据中
|
||||
const completeQuestionData = {
|
||||
...questionData,
|
||||
options: optionsData
|
||||
};
|
||||
renderQuestionData(completeQuestionData);
|
||||
} else {
|
||||
console.error('❌ 获取题目详情失败');
|
||||
message.error('获取题目详情失败');
|
||||
|
@ -6,26 +6,49 @@
|
||||
<n-button type="primary" @click="handleAddExam">添加试卷</n-button>
|
||||
<n-button ghost>导入</n-button>
|
||||
<n-button ghost>导出</n-button>
|
||||
<n-button type="error" ghost>删除</n-button>
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" @keyup.enter="handleSearch" />
|
||||
<n-button type="error" ghost @click="handleBatchDelete" :disabled="checkedRowKeys.length === 0">
|
||||
删除 ({{ checkedRowKeys.length }})
|
||||
</n-button>
|
||||
<n-input v-model:value="searchKeyword" placeholder="请输入想要搜索的内容" @keyup.enter="handleSearch"
|
||||
@input="handleRealTimeSearch" />
|
||||
<n-button type="primary" @click="handleSearch">搜索</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
<transition name="fade" mode="out-in">
|
||||
<n-data-table :columns="columns" :data="paginatedExamData" :loading="loading || isSearching"
|
||||
:row-key="(row: any) => row.id" @update:checked-row-keys="handleCheck" class="exam-table"
|
||||
:single-line="false" :pagination="paginationConfig" :key="searchKeyword">
|
||||
<template #empty>
|
||||
<div class="empty-state">
|
||||
<div v-if="isSearching" class="searching-state">
|
||||
<n-spin size="small" />
|
||||
<span style="margin-left: 8px;">搜索中...</span>
|
||||
</div>
|
||||
<div v-else-if="searchKeyword.trim()" class="no-results">
|
||||
未找到匹配的试卷
|
||||
</div>
|
||||
<div v-else>
|
||||
暂无数据
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, ref, VNode, computed, onMounted } from 'vue';
|
||||
import { NButton, NSpace, useMessage, NDataTable, NInput } from 'naive-ui';
|
||||
import { NButton, NSpace, useMessage, NDataTable, NInput, useDialog, NSpin } from 'naive-ui';
|
||||
import type { DataTableColumns } from 'naive-ui';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ExamApi } from '@/api/modules/exam';
|
||||
// import { useUserStore } from '@/stores/user';
|
||||
// import type { ExamInfo } from '@/api/types';
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
// const userStore = useUserStore();
|
||||
|
||||
// 定义考试条目的数据类型
|
||||
type Exam = {
|
||||
@ -44,24 +67,53 @@ type Exam = {
|
||||
};
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false);
|
||||
const examData = ref<Exam[]>([]);
|
||||
const searchKeyword = ref('');
|
||||
const filters = ref({
|
||||
category: '',
|
||||
status: '',
|
||||
difficulty: '',
|
||||
creator: ''
|
||||
const isSearching = ref(false);
|
||||
// 筛选条件(暂时未使用)
|
||||
// const filters = ref({
|
||||
// category: '',
|
||||
// status: '',
|
||||
// difficulty: '',
|
||||
// creator: ''
|
||||
// });
|
||||
|
||||
// 前端搜索过滤
|
||||
const filteredExamData = computed(() => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
return examData.value;
|
||||
}
|
||||
|
||||
const keyword = searchKeyword.value.toLowerCase().trim();
|
||||
return examData.value.filter(item => {
|
||||
return (
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.category.toLowerCase().includes(keyword) ||
|
||||
item.creator.toLowerCase().includes(keyword) ||
|
||||
item.chapter.toLowerCase().includes(keyword) ||
|
||||
item.status.toLowerCase().includes(keyword) ||
|
||||
item.difficulty.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// 分页数据
|
||||
const paginatedExamData = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value;
|
||||
const end = start + pageSize.value;
|
||||
return filteredExamData.value.slice(start, end);
|
||||
});
|
||||
|
||||
// 创建表格列的函数
|
||||
const createColumns = ({
|
||||
handleAction,
|
||||
}: {
|
||||
handleAction: (action: string, rowData: Exam) => void;
|
||||
}): DataTableColumns<Exam> => {
|
||||
handleAction: (action: string, rowData: any) => void;
|
||||
}): DataTableColumns<any> => {
|
||||
return [
|
||||
{
|
||||
type: 'selection',
|
||||
@ -103,7 +155,7 @@ const createColumns = ({
|
||||
{
|
||||
title: '起止时间',
|
||||
key: 'startTime',
|
||||
render(row) {
|
||||
render(row: any) {
|
||||
return `${row.startTime} - ${row.endTime}`;
|
||||
},
|
||||
},
|
||||
@ -118,7 +170,7 @@ const createColumns = ({
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
render(row: any) {
|
||||
const buttons: VNode[] = [];
|
||||
if (row.status === '发布中') {
|
||||
buttons.push(
|
||||
@ -145,53 +197,115 @@ const createColumns = ({
|
||||
];
|
||||
};
|
||||
|
||||
// 使用新的getExamInfo接口加载考试信息
|
||||
// 加载考试信息函数(暂时未使用)
|
||||
// const loadExamInfo = async () => {
|
||||
// loading.value = true;
|
||||
// try {
|
||||
// // 获取当前用户ID
|
||||
// const currentUser = userStore.user;
|
||||
// if (!currentUser || !currentUser.id) {
|
||||
// message.error('请先登录');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// console.log('🔍 获取教师考试信息,用户ID:', currentUser.id);
|
||||
// const response = await ExamApi.getExamInfo(currentUser.id.toString());
|
||||
// console.log('✅ 获取教师考试信息成功:', response);
|
||||
|
||||
// let listData: ExamInfo[] = [];
|
||||
// if (response.data && Array.isArray(response.data)) {
|
||||
// listData = response.data;
|
||||
// }
|
||||
|
||||
// // 数据映射
|
||||
// const mappedList = listData.map((item: ExamInfo) => {
|
||||
// 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 || '',
|
||||
// name: item.name || '未命名试卷',
|
||||
// category: (categoryMap[item.type] || '练习') as '练习' | '考试',
|
||||
// questionCount: 0, // 暂时设为0,需要从题目关联表获取
|
||||
// chapter: '未分类', // 暂时设为默认值
|
||||
// totalScore: 0, // 暂时设为0,需要从题目关联表计算
|
||||
// difficulty: (difficultyMap[Number(item.difficulty) || 0] || '易') 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 = mappedList.length;
|
||||
// } catch (error) {
|
||||
// console.error('加载考试信息失败:', error);
|
||||
// message.error('加载考试信息失败');
|
||||
// examData.value = [];
|
||||
// totalItems.value = 0;
|
||||
// } finally {
|
||||
// loading.value = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// 加载试卷列表
|
||||
const loadExamPaperList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
page: 1,
|
||||
pageSize: 1000 // 获取所有数据,前端进行搜索和分页
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
// 前端搜索,不需要传递搜索参数给API
|
||||
|
||||
console.log('🔍 获取试卷列表参数:', params);
|
||||
const response = await ExamApi.getExamPaperList(params);
|
||||
console.log('✅ 获取试卷列表成功:', response);
|
||||
console.log('📊 响应数据结构:', JSON.stringify(response, null, 2));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,23 +314,35 @@ const loadExamPaperList = async () => {
|
||||
listData = [];
|
||||
}
|
||||
|
||||
// 如果API没有返回数据,记录日志
|
||||
if (listData.length === 0) {
|
||||
console.log('📝 API返回空数据,当前用户没有试卷');
|
||||
}
|
||||
|
||||
// 数据映射
|
||||
const mappedList = listData.map((item: any) => {
|
||||
const statusMap: { [key: number]: string } = {
|
||||
0: '未发布',
|
||||
1: '发布中',
|
||||
2: '已结束'
|
||||
// 根据总分计算难度
|
||||
const getDifficulty = (totalScore: number): '易' | '中' | '难' => {
|
||||
if (totalScore <= 60) return '易';
|
||||
if (totalScore <= 100) return '中';
|
||||
return '难';
|
||||
};
|
||||
|
||||
const categoryMap: { [key: number]: string } = {
|
||||
0: '练习',
|
||||
1: '考试'
|
||||
// 根据组卷模式确定分类
|
||||
const getCategory = (generateMode: number | null): '练习' | '考试' => {
|
||||
if (generateMode === 0) return '考试'; // 固定试卷组
|
||||
if (generateMode === 1) return '练习'; // 随机抽题组卷
|
||||
return '考试'; // 默认为考试
|
||||
};
|
||||
|
||||
const difficultyMap: { [key: number]: string } = {
|
||||
0: '易',
|
||||
1: '中',
|
||||
2: '难'
|
||||
// 根据试卷信息推断状态
|
||||
const getStatus = (item: any): '发布中' | '未发布' | '已结束' => {
|
||||
// 如果有标题且总分大于0,认为是已发布的试卷
|
||||
if (item.title && item.totalScore > 0) {
|
||||
return '发布中';
|
||||
}
|
||||
// 如果标题为空或总分为0,认为是未完成的试卷
|
||||
return '未发布';
|
||||
};
|
||||
|
||||
// 格式化时间显示
|
||||
@ -232,15 +358,15 @@ const loadExamPaperList = async () => {
|
||||
};
|
||||
|
||||
return {
|
||||
id: item.id || item.paperId || '',
|
||||
name: item.name || '未命名试卷',
|
||||
category: (categoryMap[item.type] || '练习') as '练习' | '考试',
|
||||
id: item.id || '',
|
||||
name: item.title || '未命名试卷',
|
||||
category: getCategory(item.generateMode),
|
||||
questionCount: 0, // 暂时设为0,需要从题目关联表获取
|
||||
chapter: '未分类', // 暂时设为默认值
|
||||
totalScore: 0, // 暂时设为0,需要从题目关联表计算
|
||||
difficulty: (difficultyMap[item.difficulty] || '易') as '易' | '中' | '难',
|
||||
status: (statusMap[item.status] || '未发布') as '发布中' | '未发布' | '已结束',
|
||||
startTime: formatTime(item.startTime, item.endTime),
|
||||
totalScore: item.totalScore || 0,
|
||||
difficulty: getDifficulty(item.totalScore || 0),
|
||||
status: getStatus(item),
|
||||
startTime: formatTime(item.startTime || '', item.endTime || ''),
|
||||
endTime: item.endTime || '',
|
||||
creator: item.createBy || '未知',
|
||||
creationTime: item.createTime || ''
|
||||
@ -248,7 +374,7 @@ const loadExamPaperList = async () => {
|
||||
});
|
||||
|
||||
examData.value = mappedList;
|
||||
totalItems.value = totalCount;
|
||||
totalItems.value = mappedList.length; // 前端分页,总数就是数据长度
|
||||
} catch (error) {
|
||||
console.error('加载试卷列表失败:', error);
|
||||
message.error('加载试卷列表失败');
|
||||
@ -260,41 +386,53 @@ const loadExamPaperList = async () => {
|
||||
};
|
||||
|
||||
const columns = createColumns({
|
||||
handleAction: (action, row) => {
|
||||
if(action === '试卷分析'){
|
||||
// 根据当前路由上下文决定跳转路径
|
||||
const currentRoute = route.path;
|
||||
if (currentRoute.includes('/course-editor/')) {
|
||||
const courseId = route.params.id;
|
||||
router.push(`/teacher/course-editor/${courseId}/practice/exam/analysis?examId=${row.id}`);
|
||||
} else {
|
||||
router.push({ name: 'ExamAnalysis', query: { examId: row.id } });
|
||||
handleAction: (action: string, row: any) => {
|
||||
try {
|
||||
if (action === '试卷分析') {
|
||||
// 根据当前路由上下文决定跳转路径
|
||||
const currentRoute = route.path;
|
||||
if (currentRoute.includes('/course-editor/')) {
|
||||
const courseId = route.params.id;
|
||||
router.push(`/teacher/course-editor/${courseId}/practice/exam/analysis?examId=${row.id}`);
|
||||
} else {
|
||||
// 暂时显示提示,因为试卷分析页面可能还未实现
|
||||
message.info('试卷分析功能正在开发中');
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(action === '批阅'){
|
||||
// 根据当前路由上下文决定跳转路径
|
||||
const currentRoute = route.path;
|
||||
if (currentRoute.includes('/course-editor/')) {
|
||||
const courseId = route.params.id;
|
||||
router.push(`/teacher/course-editor/${courseId}/practice/review/student-list/${row.id}`);
|
||||
} else {
|
||||
router.push({ name: 'StudentList', params: { paperId: row.id } });
|
||||
if (action === '批阅') {
|
||||
// 根据当前路由上下文决定跳转路径
|
||||
const currentRoute = route.path;
|
||||
if (currentRoute.includes('/course-editor/')) {
|
||||
const courseId = route.params.id;
|
||||
router.push(`/teacher/course-editor/${courseId}/practice/review/student-list/${row.id}`);
|
||||
} else {
|
||||
// 暂时显示提示,因为批阅页面可能还未实现
|
||||
message.info('批阅功能正在开发中');
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(action === '编辑'){
|
||||
// 根据当前路由上下文决定跳转路径
|
||||
const currentRoute = route.path;
|
||||
if (currentRoute.includes('/course-editor/')) {
|
||||
const courseId = route.params.id;
|
||||
router.push(`/teacher/course-editor/${courseId}/practice/exam/edit/${row.id}`);
|
||||
} else {
|
||||
router.push({ name: 'EditExam', params: { id: row.id } });
|
||||
if (action === '编辑') {
|
||||
// 根据当前路由上下文决定跳转路径
|
||||
const currentRoute = route.path;
|
||||
if (currentRoute.includes('/course-editor/')) {
|
||||
const courseId = route.params.id;
|
||||
router.push(`/teacher/course-editor/${courseId}/practice/exam/edit/${row.id}`);
|
||||
} else {
|
||||
// 跳转到编辑试卷页面
|
||||
router.push(`/teacher/exam-management/edit/${row.id}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
if (action === '删除') {
|
||||
handleDeletePaper(row);
|
||||
return;
|
||||
}
|
||||
message.info(`执行操作: ${action} on row ${row.id}`);
|
||||
} catch (error) {
|
||||
console.error('处理操作时发生错误:', error);
|
||||
message.error('操作失败,请重试');
|
||||
}
|
||||
message.info(`执行操作: ${action} on row ${row.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
@ -312,7 +450,7 @@ const totalItems = ref(0);
|
||||
const paginationConfig = computed(() => ({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
itemCount: totalItems.value,
|
||||
itemCount: filteredExamData.value.length,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
showSizePicker: true,
|
||||
showQuickJumper: true,
|
||||
@ -322,37 +460,164 @@ const paginationConfig = computed(() => ({
|
||||
},
|
||||
onUpdatePage: (page: number) => {
|
||||
currentPage.value = page;
|
||||
loadExamPaperList();
|
||||
},
|
||||
onUpdatePageSize: (newPageSize: number) => {
|
||||
pageSize.value = newPageSize;
|
||||
currentPage.value = 1;
|
||||
loadExamPaperList();
|
||||
}
|
||||
}));
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = () => {
|
||||
// 防抖定时器
|
||||
let searchTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
// 实时搜索功能
|
||||
const handleRealTimeSearch = () => {
|
||||
// 清除之前的定时器
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
|
||||
// 设置新的定时器,300ms后执行搜索
|
||||
searchTimeout = setTimeout(() => {
|
||||
handleSearch();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
// 搜索功能 - 前端搜索,不需要重新请求API
|
||||
const handleSearch = async () => {
|
||||
// 搜索时重置到第一页
|
||||
currentPage.value = 1;
|
||||
loadExamPaperList();
|
||||
|
||||
// 添加搜索过渡效果
|
||||
if (searchKeyword.value.trim()) {
|
||||
isSearching.value = true;
|
||||
// 模拟搜索延迟,提供视觉反馈
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
isSearching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleAddExam = () => {
|
||||
// 根据当前路由上下文决定跳转路径
|
||||
const currentRoute = route.path;
|
||||
if (currentRoute.includes('/course-editor/')) {
|
||||
// 如果在课程编辑器中,使用 practice 路径
|
||||
const courseId = route.params.id;
|
||||
router.push(`/teacher/course-editor/${courseId}/practice/exam/add`);
|
||||
} else {
|
||||
// 如果在考试管理中,使用原有路径
|
||||
router.push({ name: 'AddExam' });
|
||||
try {
|
||||
// 根据当前路由上下文决定跳转路径
|
||||
const currentRoute = route.path;
|
||||
if (currentRoute.includes('/course-editor/')) {
|
||||
// 如果在课程编辑器中,使用 practice 路径
|
||||
const courseId = route.params.id;
|
||||
router.push(`/teacher/course-editor/${courseId}/practice/exam/add`);
|
||||
} else {
|
||||
// 如果在考试管理中,跳转到添加试卷页面
|
||||
router.push('/teacher/exam-management/add');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('跳转到添加试卷页面时发生错误:', error);
|
||||
message.error('跳转失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 删除试卷
|
||||
const handleDeletePaper = async (row: any) => {
|
||||
try {
|
||||
// 使用 Naive UI 对话框组件
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: `确定要删除试卷"${row.name}"吗?此操作不可撤销。`,
|
||||
positiveText: '确定删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
console.log('🗑️ 开始删除试卷:', row.id);
|
||||
|
||||
// 调用删除API
|
||||
const response = await ExamApi.deleteExamPaper(row.id);
|
||||
console.log('✅ 删除试卷成功:', response);
|
||||
|
||||
// 显示成功消息
|
||||
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 {
|
||||
console.log('🗑️ 开始批量删除试卷:', checkedRowKeys.value);
|
||||
|
||||
// 调用批量删除API
|
||||
const response = await ExamApi.batchDeleteExamPapers(checkedRowKeys.value as string[]);
|
||||
console.log('✅ 批量删除试卷完成:', response);
|
||||
|
||||
// 根据删除结果显示不同的消息
|
||||
if (response.data) {
|
||||
const { success, failed, errors } = response.data;
|
||||
|
||||
if (failed === 0) {
|
||||
// 全部成功
|
||||
message.success(`成功删除 ${success} 个试卷!`);
|
||||
} else if (success > 0) {
|
||||
// 部分成功
|
||||
message.warning(`删除完成:成功 ${success} 个,失败 ${failed} 个`);
|
||||
if (errors.length > 0) {
|
||||
console.warn('删除失败的试卷:', errors);
|
||||
}
|
||||
} else {
|
||||
// 全部失败
|
||||
message.error(`删除失败:${failed} 个试卷删除失败`);
|
||||
if (errors.length > 0) {
|
||||
console.error('删除错误详情:', errors);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.success(`成功删除 ${checkedRowKeys.value.length} 个试卷!`);
|
||||
}
|
||||
|
||||
// 清空选中状态
|
||||
checkedRowKeys.value = [];
|
||||
|
||||
// 重新加载试卷列表
|
||||
await loadExamPaperList();
|
||||
} catch (error) {
|
||||
console.error('批量删除试卷失败:', error);
|
||||
message.error('批量删除试卷失败,请重试');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量删除试卷失败:', error);
|
||||
message.error('批量删除试卷失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
// 使用试卷列表接口
|
||||
loadExamPaperList();
|
||||
});
|
||||
|
||||
@ -388,4 +653,45 @@ onMounted(() => {
|
||||
.exam-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 搜索过渡效果 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.searching-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
@ -16,8 +16,18 @@
|
||||
</div>
|
||||
|
||||
<n-data-table ref="tableRef" :columns="columns" :data="questionBankList" :loading="loading"
|
||||
:pagination="paginationConfig" :row-key="(row: QuestionBank) => row.id" :checked-row-keys="selectedRowKeys"
|
||||
@update:checked-row-keys="handleCheck" class="question-bank-table" :single-line="false" />
|
||||
:row-key="(row: QuestionBank) => row.id" :checked-row-keys="selectedRowKeys"
|
||||
@update:checked-row-keys="handleCheck" class="question-bank-table" :single-line="false" :key="paginationKey"
|
||||
@contextmenu="handleContextMenu" />
|
||||
|
||||
<!-- 独立的分页器 -->
|
||||
<div style="margin-top: 16px; display: flex; justify-content: flex-end;">
|
||||
<n-pagination v-model:page="pagination.page" v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.total" :page-sizes="[10, 20, 50, 100]" show-size-picker show-quick-jumper
|
||||
:prefix="(info: any) => `共 ${info.itemCount} 条`" @update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange" />
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 新建/编辑题库弹窗 -->
|
||||
<n-modal v-model:show="showCreateModal" preset="dialog" :title="isEditMode ? '编辑题库' : '新建题库'"
|
||||
@ -53,17 +63,9 @@
|
||||
<p class="import-tip">请选择要导入的Excel文件(支持.xlsx, .xls格式)</p>
|
||||
</div>
|
||||
|
||||
<n-upload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="importFileList"
|
||||
:max="1"
|
||||
accept=".xlsx,.xls"
|
||||
:custom-request="handleImportUpload"
|
||||
@change="handleImportFileChange"
|
||||
show-file-list
|
||||
list-type="text"
|
||||
:default-upload="false"
|
||||
>
|
||||
<n-upload ref="uploadRef" v-model:file-list="importFileList" :max="1" accept=".xlsx,.xls"
|
||||
:custom-request="handleImportUpload" @change="handleImportFileChange" show-file-list
|
||||
list-type="text" :default-upload="false">
|
||||
<n-upload-dragger>
|
||||
<div style="margin-bottom: 12px">
|
||||
<n-icon size="48" :depth="3">
|
||||
@ -92,12 +94,7 @@
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="closeImportModal">取消</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="startImport"
|
||||
:loading="importing"
|
||||
:disabled="!selectedImportFile"
|
||||
>
|
||||
<n-button type="primary" @click="startImport" :loading="importing" :disabled="!selectedImportFile">
|
||||
{{ importing ? '导入中...' : '开始导入' }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
@ -107,7 +104,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
|
||||
import { ref, reactive, onMounted, h, VNode, watch, nextTick } from 'vue';
|
||||
import { NButton, NSpace, NSelect, NIcon, NText, NP, NAlert, NUpload, NUploadDragger, useMessage, useDialog } from 'naive-ui';
|
||||
import { CloudUploadOutline } from '@vicons/ionicons5';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
@ -154,6 +151,12 @@ const loading = ref(false);
|
||||
const selectedRowKeys = ref<string[]>([]);
|
||||
const questionBankList = ref<QuestionBank[]>([]);
|
||||
|
||||
// 右键菜单相关状态(暂时未使用)
|
||||
// const contextMenuVisible = ref(false);
|
||||
// const contextMenuX = ref(0);
|
||||
// const contextMenuY = ref(0);
|
||||
// const contextMenuRow = ref<QuestionBank | null>(null);
|
||||
|
||||
// 课程列表状态
|
||||
const courseOptions = ref<CourseOption[]>([]);
|
||||
|
||||
@ -187,28 +190,17 @@ const pagination = reactive({
|
||||
total: 0
|
||||
});
|
||||
|
||||
// 表格分页配置
|
||||
const paginationConfig = computed(() => ({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
itemCount: pagination.total,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
showSizePicker: true,
|
||||
showQuickJumper: true,
|
||||
prefix: (info: { itemCount?: number }) => {
|
||||
const itemCount = info.itemCount || 0;
|
||||
return `共 ${itemCount} 条`;
|
||||
},
|
||||
onUpdatePage: (page: number) => {
|
||||
pagination.page = page;
|
||||
loadQuestionBanks();
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
loadQuestionBanks();
|
||||
}
|
||||
}));
|
||||
// 强制更新分页器的key
|
||||
const paginationKey = ref(0);
|
||||
|
||||
// 监听pagination.total的变化,强制更新分页器
|
||||
watch(() => pagination.total, () => {
|
||||
nextTick(() => {
|
||||
paginationKey.value++;
|
||||
});
|
||||
});
|
||||
|
||||
// 注意:paginationConfig 已不再使用,因为我们现在使用独立的 n-pagination 组件
|
||||
|
||||
// 创建表格列的函数
|
||||
const createColumns = ({
|
||||
@ -313,6 +305,15 @@ const createColumns = ({
|
||||
}, { default: () => '编辑' })
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
ghost: true,
|
||||
onClick: () => handleAction('导出', row)
|
||||
}, { default: () => '导出' })
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
h(NButton, {
|
||||
size: 'small',
|
||||
@ -335,6 +336,8 @@ const columns = createColumns({
|
||||
enterQuestionBank(row.id, row.name);
|
||||
} else if (action === '编辑') {
|
||||
editQuestionBank(row.id);
|
||||
} else if (action === '导出') {
|
||||
exportQuestionBank();
|
||||
} else if (action === '删除') {
|
||||
deleteQuestionBank(row.id);
|
||||
}
|
||||
@ -352,6 +355,18 @@ const searchQuestionBanks = () => {
|
||||
loadQuestionBanks();
|
||||
};
|
||||
|
||||
// 分页器事件处理
|
||||
const handlePageChange = (page: number) => {
|
||||
pagination.page = page;
|
||||
loadQuestionBanks();
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (pageSize: number) => {
|
||||
pagination.pageSize = pageSize;
|
||||
pagination.page = 1;
|
||||
loadQuestionBanks();
|
||||
};
|
||||
|
||||
// 根据课程ID获取课程名称
|
||||
const getCourseNameById = (courseId: string): string => {
|
||||
const course = courseOptions.value.find(option => option.value === courseId);
|
||||
@ -799,6 +814,12 @@ const closeImportModal = () => {
|
||||
// message.info('模板下载功能开发中...');
|
||||
// };
|
||||
|
||||
// 右键菜单处理
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
// 右键菜单功能可以在这里实现
|
||||
};
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadCourseList();
|
||||
|
@ -426,29 +426,7 @@ const columns = createColumns({
|
||||
},
|
||||
});
|
||||
|
||||
// 生成模拟数据
|
||||
const generateMockData = (): Question[] => {
|
||||
const mockData: Question[] = [];
|
||||
const types = ['single_choice', 'multiple_choice', 'true_false', 'fill_blank', 'short_answer'];
|
||||
const difficulties = ['easy', 'medium', 'hard'];
|
||||
const categories = customCategories.value.map(cat => cat.value);
|
||||
const creators = ['王建国', '李明', '张三', '刘老师'];
|
||||
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
mockData.push({
|
||||
id: `question_${i}`,
|
||||
sequence: i,
|
||||
title: `在教育中的优势条件下各部门,内容关于...`,
|
||||
type: types[Math.floor(Math.random() * types.length)],
|
||||
category: categories[Math.floor(Math.random() * categories.length)],
|
||||
difficulty: difficulties[Math.floor(Math.random() * difficulties.length)],
|
||||
score: 10,
|
||||
creator: creators[Math.floor(Math.random() * creators.length)],
|
||||
createTime: '2025.08.20 09:20'
|
||||
});
|
||||
}
|
||||
return mockData;
|
||||
};
|
||||
// 移除模拟数据生成函数
|
||||
|
||||
// 处理复选框选择
|
||||
const handleCheck = (rowKeys: string[]) => {
|
||||
@ -504,7 +482,14 @@ const loadQuestions = async () => {
|
||||
|
||||
// 处理API响应数据
|
||||
const apiResponse = response.data as any;
|
||||
if (apiResponse && (apiResponse.code === 200 || apiResponse.code === 0) && apiResponse.result) {
|
||||
|
||||
// 检查是否是错误响应(没有题目或题库不存在)
|
||||
if (apiResponse && apiResponse.success === false &&
|
||||
(apiResponse.message?.includes('该题库下没有题目') ||
|
||||
apiResponse.message?.includes('该题库不存在'))) {
|
||||
console.log('📝 题库没有题目,显示空状态');
|
||||
allData = [];
|
||||
} else if (apiResponse && (apiResponse.code === 200 || apiResponse.code === 0) && apiResponse.result) {
|
||||
// 将API返回的数据转换为前端格式
|
||||
allData = apiResponse.result.map((item: any, index: number) => ({
|
||||
id: item.id || `question_${index}`,
|
||||
@ -521,8 +506,8 @@ const loadQuestions = async () => {
|
||||
}));
|
||||
console.log('✅ 题目数据转换成功:', allData.length, '条题目');
|
||||
} else {
|
||||
console.warn('⚠️ 题目列表API返回异常,使用模拟数据');
|
||||
allData = generateMockData();
|
||||
console.warn('⚠️ 题目列表API返回异常,无数据');
|
||||
allData = [];
|
||||
}
|
||||
|
||||
// 应用筛选条件
|
||||
@ -547,27 +532,24 @@ const loadQuestions = async () => {
|
||||
|
||||
console.log('📊 题目列表加载完成:', questionList.value.length, '条题目');
|
||||
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('❌ 加载题目失败:', error);
|
||||
// 出错时使用模拟数据
|
||||
const allData = generateMockData();
|
||||
let filteredData = allData;
|
||||
|
||||
if (filters.category) {
|
||||
filteredData = filteredData.filter(item => item.category.includes(filters.category));
|
||||
// 检查是否是"没有题目"的情况
|
||||
if (error?.message?.includes('该题库下没有题目') ||
|
||||
error?.message?.includes('该题库不存在') ||
|
||||
error?.response?.data?.message?.includes('该题库下没有题目') ||
|
||||
error?.response?.data?.message?.includes('该题库不存在')) {
|
||||
console.log('📝 题库没有题目,显示空状态');
|
||||
questionList.value = [];
|
||||
pagination.total = 0;
|
||||
// 不显示错误提示,这是正常情况
|
||||
} else {
|
||||
// 其他错误才显示错误提示
|
||||
message.error('加载题目失败,请稍后重试');
|
||||
questionList.value = [];
|
||||
pagination.total = 0;
|
||||
}
|
||||
|
||||
if (filters.keyword) {
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.title.includes(filters.keyword) ||
|
||||
item.creator.includes(filters.keyword)
|
||||
);
|
||||
}
|
||||
|
||||
pagination.total = filteredData.length;
|
||||
const start = (pagination.page - 1) * pagination.pageSize;
|
||||
const end = start + pagination.pageSize;
|
||||
questionList.value = filteredData.slice(start, end);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -591,21 +573,106 @@ const importQuestions = () => {
|
||||
showImportModal.value = true;
|
||||
};
|
||||
|
||||
const exportQuestions = () => {
|
||||
console.log('导出题目');
|
||||
const exportQuestions = async () => {
|
||||
try {
|
||||
console.log('🚀 开始导出题目,题库ID:', currentBankId.value);
|
||||
message.loading('正在导出题目,请稍候...', { duration: 0 });
|
||||
|
||||
const response = await ExamApi.exportQuestions(currentBankId.value);
|
||||
|
||||
if (!response || !(response instanceof Blob)) {
|
||||
throw new Error('服务器返回的数据格式不正确');
|
||||
}
|
||||
|
||||
if (response.size === 0) {
|
||||
throw new Error('题目文件为空');
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(response);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${currentBankTitle.value || '题库'}_题目.xlsx`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
message.destroyAll();
|
||||
message.success('题目导出成功!');
|
||||
|
||||
} catch (error: any) {
|
||||
message.destroyAll();
|
||||
console.error('❌ 导出题目失败:', error);
|
||||
message.error(error.message || '导出题目失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理导入成功
|
||||
const handleImportSuccess = (result: any) => {
|
||||
const handleImportSuccess = async (result: any) => {
|
||||
console.log('导入成功:', result);
|
||||
// 重新加载数据
|
||||
loadQuestions();
|
||||
try {
|
||||
if (result.file) {
|
||||
message.loading('正在处理导入文件...', { duration: 0 });
|
||||
|
||||
// 创建FormData
|
||||
const formData = new FormData();
|
||||
formData.append('file', result.file);
|
||||
|
||||
// 调用导入API
|
||||
const response = await ExamApi.importQuestions(currentBankId.value, formData);
|
||||
console.log('📊 导入题目API响应:', response);
|
||||
|
||||
message.destroyAll();
|
||||
message.success('题目导入成功!');
|
||||
|
||||
// 重新加载数据
|
||||
await loadQuestions();
|
||||
} else {
|
||||
message.success('题目导入成功!');
|
||||
await loadQuestions();
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.destroyAll();
|
||||
console.error('❌ 导入处理失败:', error);
|
||||
message.error(error.message || '导入处理失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理模板下载
|
||||
const handleTemplateDownload = (type?: string) => {
|
||||
console.log('下载模板:', type);
|
||||
// TODO: 实现模板下载API调用
|
||||
const handleTemplateDownload = async (type?: string) => {
|
||||
try {
|
||||
console.log('🚀 下载题目模板:', type);
|
||||
message.loading('正在下载模板...', { duration: 0 });
|
||||
|
||||
const response = await ExamApi.downloadTemplate();
|
||||
|
||||
if (!response || !(response instanceof Blob)) {
|
||||
throw new Error('服务器返回的数据格式不正确');
|
||||
}
|
||||
|
||||
if (response.size === 0) {
|
||||
throw new Error('模板文件为空');
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(response);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '题目导入模板.xlsx';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
message.destroyAll();
|
||||
message.success('模板下载成功!');
|
||||
|
||||
} catch (error: any) {
|
||||
message.destroyAll();
|
||||
console.error('❌ 下载模板失败:', error);
|
||||
message.error(error.message || '模板下载失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSelected = () => {
|
||||
@ -616,43 +683,118 @@ const editQuestion = async (id: string) => {
|
||||
console.log('🔍 编辑题目,题目ID:', id);
|
||||
|
||||
try {
|
||||
// 先调用题目详情接口获取完整信息
|
||||
console.log('🚀 调用题目详情接口...');
|
||||
const response = await ExamApi.getQuestionDetail(id);
|
||||
console.log('📊 题目详情API响应:', response);
|
||||
// 并行获取题目详情和选项数据
|
||||
console.log('🚀 调用题目详情和选项接口...');
|
||||
console.log('🔍 题目ID:', id);
|
||||
|
||||
// 处理API响应,支持不同的响应格式
|
||||
const [questionResponse, optionsResponse] = await Promise.allSettled([
|
||||
ExamApi.getQuestionDetail(id),
|
||||
ExamApi.getQuestionOptions(id)
|
||||
]);
|
||||
|
||||
// 处理题目详情响应
|
||||
let questionData = null;
|
||||
let success = false;
|
||||
|
||||
if (response.data) {
|
||||
// 使用类型断言处理API响应
|
||||
const apiResponse = response.data as any;
|
||||
if (questionResponse.status === 'fulfilled') {
|
||||
const response = questionResponse.value;
|
||||
console.log('📊 题目详情API响应:', response);
|
||||
|
||||
// 检查是否是包装格式 {success, code, result}
|
||||
if (typeof apiResponse === 'object' && 'result' in apiResponse) {
|
||||
success = apiResponse.success === true || apiResponse.code === 200 || apiResponse.code === 0;
|
||||
questionData = apiResponse.result;
|
||||
if (response.data) {
|
||||
const apiResponse = response.data as any;
|
||||
console.log('🔍 题目详情API原始响应:', apiResponse);
|
||||
|
||||
// 检查是否是包装格式 {success, code, result}
|
||||
if (typeof apiResponse === 'object' && 'result' in apiResponse) {
|
||||
success = apiResponse.success === true || apiResponse.code === 200 || apiResponse.code === 0;
|
||||
questionData = apiResponse.result;
|
||||
} else {
|
||||
// 直接是题目数据
|
||||
success = true;
|
||||
questionData = apiResponse;
|
||||
}
|
||||
|
||||
console.log('🔍 解析后的题目数据:', questionData);
|
||||
} else {
|
||||
// 直接是题目数据
|
||||
success = true;
|
||||
questionData = apiResponse;
|
||||
console.warn('⚠️ 题目详情API响应数据为空');
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 题目详情API调用失败:', questionResponse.reason);
|
||||
}
|
||||
|
||||
// 处理选项数据
|
||||
let optionsData = [];
|
||||
|
||||
if (optionsResponse.status === 'fulfilled') {
|
||||
const response = optionsResponse.value;
|
||||
console.log('📊 题目选项API响应:', response);
|
||||
|
||||
if (response.data) {
|
||||
const apiResponse = response.data as any;
|
||||
console.log('🔍 选项API原始响应:', apiResponse);
|
||||
console.log('🔍 选项API result字段:', apiResponse.result);
|
||||
console.log('🔍 选项API result类型:', typeof apiResponse.result);
|
||||
console.log('🔍 选项API result是否为数组:', Array.isArray(apiResponse.result));
|
||||
|
||||
if (Array.isArray(apiResponse)) {
|
||||
optionsData = apiResponse;
|
||||
console.log('✅ 选项数据是数组格式');
|
||||
} else if (apiResponse && Array.isArray(apiResponse.result)) {
|
||||
optionsData = apiResponse.result;
|
||||
console.log('✅ 选项数据在result字段中');
|
||||
} else if (apiResponse && apiResponse.success && Array.isArray(apiResponse.result)) {
|
||||
optionsData = apiResponse.result;
|
||||
console.log('✅ 选项数据在success.result字段中');
|
||||
} else if (apiResponse && apiResponse.success && apiResponse.result) {
|
||||
// 如果result不是数组,可能是单个对象,需要转换为数组
|
||||
if (Array.isArray(apiResponse.result)) {
|
||||
optionsData = apiResponse.result;
|
||||
console.log('✅ 选项数据在success.result字段中(数组)');
|
||||
} else if (apiResponse.result && apiResponse.result.records) {
|
||||
// 如果是分页格式,取records字段
|
||||
optionsData = apiResponse.result.records || [];
|
||||
console.log('✅ 选项数据在分页格式的records字段中');
|
||||
} else {
|
||||
// 如果是单个对象,转换为数组
|
||||
optionsData = [apiResponse.result];
|
||||
console.log('✅ 选项数据是单个对象,转换为数组');
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ 选项数据格式未知:', apiResponse);
|
||||
}
|
||||
|
||||
console.log('🔍 解析后的选项数据:', optionsData);
|
||||
} else {
|
||||
console.warn('⚠️ 选项API响应数据为空');
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 题目选项API调用失败:', optionsResponse.reason);
|
||||
}
|
||||
|
||||
if (success && questionData) {
|
||||
console.log('✅ 获取题目详情成功:', questionData);
|
||||
console.log('✅ 获取选项数据成功:', optionsData);
|
||||
console.log('🔍 选项数据长度:', optionsData.length);
|
||||
|
||||
// 跳转到编辑页面,并传递题目详情数据
|
||||
// 构建完整的题目数据,包含选项
|
||||
const completeQuestionData = {
|
||||
question: questionData,
|
||||
answer: optionsData, // 将选项数据作为answer传递
|
||||
children: [] // 复合题的子题目数据
|
||||
};
|
||||
|
||||
console.log('🔍 完整题目数据:', completeQuestionData);
|
||||
|
||||
// 跳转到编辑页面,并传递完整的题目数据
|
||||
router.push({
|
||||
path: `/teacher/exam-management/add-question/${currentBankId.value}/${id}`,
|
||||
query: {
|
||||
questionData: JSON.stringify(questionData),
|
||||
questionData: JSON.stringify(completeQuestionData),
|
||||
mode: 'edit'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('❌ 获取题目详情失败:', response);
|
||||
console.error('❌ 获取题目详情失败:', questionResponse);
|
||||
message.error('获取题目详情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user