feat:对naive ui添加国际化配置;添加考试相关api文件;考试管理菜单下逻辑优化完善;6种题型全部提取为独立组件。
This commit is contained in:
parent
cea9929ebc
commit
3d4fa1abb4
36
src/App.vue
36
src/App.vue
@ -3,8 +3,14 @@ import { onMounted, computed } from 'vue'
|
|||||||
import { RouterView, useRoute } from 'vue-router'
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||||
import { NConfigProvider, NMessageProvider } from 'naive-ui'
|
import { NConfigProvider, NMessageProvider, NDialogProvider, zhCN, dateZhCN, enUS, dateEnUS } from 'naive-ui'
|
||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
// 响应式获取naive-ui语言包
|
||||||
|
const naiveLocale = computed(() => locale.value === 'en' ? enUS : zhCN)
|
||||||
|
const naiveDateLocale = computed(() => locale.value === 'en' ? dateEnUS : dateZhCN)
|
||||||
|
|
||||||
|
|
||||||
// 自定义naive-ui主题颜色
|
// 自定义naive-ui主题颜色
|
||||||
@ -71,19 +77,21 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<n-config-provider :theme-overrides="themeOverrides">
|
<n-config-provider :theme-overrides="themeOverrides" :locale="naiveLocale" :date-locale="naiveDateLocale">
|
||||||
<!-- 登录页面不使用 AppLayout,但需要 message provider -->
|
<n-dialog-provider>
|
||||||
<template v-if="isLoginPage">
|
<!-- 登录页面不使用 AppLayout,但需要 message provider -->
|
||||||
<n-message-provider>
|
<template v-if="isLoginPage">
|
||||||
<RouterView />
|
<n-message-provider>
|
||||||
</n-message-provider>
|
<RouterView />
|
||||||
</template>
|
</n-message-provider>
|
||||||
<!-- 其他页面使用 AppLayout -->
|
</template>
|
||||||
<template v-else>
|
<!-- 其他页面使用 AppLayout -->
|
||||||
<AppLayout>
|
<template v-else>
|
||||||
<RouterView />
|
<AppLayout>
|
||||||
</AppLayout>
|
<RouterView />
|
||||||
</template>
|
</AppLayout>
|
||||||
|
</template>
|
||||||
|
</n-dialog-provider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -10,6 +10,7 @@ export { default as FavoriteApi } from './modules/favorite'
|
|||||||
export { default as OrderApi } from './modules/order'
|
export { default as OrderApi } from './modules/order'
|
||||||
export { default as UploadApi } from './modules/upload'
|
export { default as UploadApi } from './modules/upload'
|
||||||
export { default as StatisticsApi } from './modules/statistics'
|
export { default as StatisticsApi } from './modules/statistics'
|
||||||
|
export { default as ExamApi } from './modules/exam'
|
||||||
|
|
||||||
// API 基础配置
|
// API 基础配置
|
||||||
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot'
|
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot'
|
||||||
@ -182,6 +183,37 @@ export const API_ENDPOINTS = {
|
|||||||
COMMENTS: '/statistics/comments',
|
COMMENTS: '/statistics/comments',
|
||||||
EXPORT: '/statistics/export/:type',
|
EXPORT: '/statistics/export/:type',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 考试题库相关
|
||||||
|
EXAM: {
|
||||||
|
// 题库管理
|
||||||
|
REPO_CREATE: '/biz/repo/courseAdd',
|
||||||
|
REPO_LIST: '/biz/repo/repoList',
|
||||||
|
REPO_DELETE: '/gen/repo/repo/delete',
|
||||||
|
REPO_EDIT: '/gen/repo/repo/edit',
|
||||||
|
|
||||||
|
// 题目管理
|
||||||
|
QUESTION_LIST: '/biz/repo/questionList/:repoId',
|
||||||
|
QUESTION_DETAIL: '/biz/repo/repoList/:questionId',
|
||||||
|
QUESTION_ADD: '/gen/question/question/add',
|
||||||
|
QUESTION_EDIT: '/gen/question/question/edit',
|
||||||
|
QUESTION_DELETE: '/gen/question/question/delete',
|
||||||
|
|
||||||
|
// 题目选项管理
|
||||||
|
QUESTION_OPTION_ADD: '/gen/questionoption/questionOption/add',
|
||||||
|
QUESTION_OPTION_EDIT: '/gen/questionoption/questionOption/edit',
|
||||||
|
QUESTION_OPTION_DELETE: '/gen/questionoption/questionOption/delete',
|
||||||
|
|
||||||
|
// 题目答案管理
|
||||||
|
QUESTION_ANSWER_ADD: '/gen/questionanswer/questionAnswer/add',
|
||||||
|
QUESTION_ANSWER_EDIT: '/gen/questionanswer/questionAnswer/edit',
|
||||||
|
QUESTION_ANSWER_DELETE: '/gen/questionanswer/questionAnswer/delete',
|
||||||
|
|
||||||
|
// 题库题目关联管理
|
||||||
|
QUESTION_REPO_ADD: '/gen/questionrepo/questionRepo/add',
|
||||||
|
QUESTION_REPO_EDIT: '/gen/questionrepo/questionRepo/edit',
|
||||||
|
QUESTION_REPO_DELETE: '/gen/questionrepo/questionRepo/delete',
|
||||||
|
},
|
||||||
|
|
||||||
// 学习进度相关
|
// 学习进度相关
|
||||||
LEARNING: {
|
LEARNING: {
|
||||||
|
289
src/api/modules/exam.ts
Normal file
289
src/api/modules/exam.ts
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
// 考试题库相关API接口
|
||||||
|
import { ApiRequest } from '../request'
|
||||||
|
import type {
|
||||||
|
ApiResponse,
|
||||||
|
Repo,
|
||||||
|
Question,
|
||||||
|
CreateRepoRequest,
|
||||||
|
UpdateRepoRequest,
|
||||||
|
CreateQuestionRequest,
|
||||||
|
UpdateQuestionRequest,
|
||||||
|
CreateQuestionOptionRequest,
|
||||||
|
UpdateQuestionOptionRequest,
|
||||||
|
CreateQuestionAnswerRequest,
|
||||||
|
UpdateQuestionAnswerRequest,
|
||||||
|
CreateQuestionRepoRequest,
|
||||||
|
UpdateQuestionRepoRequest,
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考试题库API模块
|
||||||
|
*/
|
||||||
|
export class ExamApi {
|
||||||
|
|
||||||
|
// ========== 题库管理 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程新建题库
|
||||||
|
*/
|
||||||
|
static async createCourseRepo(data: CreateRepoRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 创建课程题库:', data)
|
||||||
|
const response = await ApiRequest.post<string>('/biz/repo/courseAdd', data)
|
||||||
|
console.log('✅ 创建课程题库成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取课程题库
|
||||||
|
*/
|
||||||
|
static async getCourseRepoList(): Promise<ApiResponse<Repo[]>> {
|
||||||
|
const response = await ApiRequest.get<Repo[]>(`/biz/repo/repoList`)
|
||||||
|
console.log('✅ 获取课程题库列表成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除题库
|
||||||
|
*/
|
||||||
|
static async deleteRepo(id: string): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 删除题库:', { id })
|
||||||
|
const response = await ApiRequest.delete<string>('/gen/repo/repo/delete', {
|
||||||
|
params: { id }
|
||||||
|
})
|
||||||
|
console.log('✅ 删除题库成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑题库
|
||||||
|
*/
|
||||||
|
static async updateRepo(data: UpdateRepoRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 编辑题库:', data)
|
||||||
|
const response = await ApiRequest.put<string>('/gen/repo/repo/edit', data)
|
||||||
|
console.log('✅ 编辑题库成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 题目管理 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询题库下题目
|
||||||
|
*/
|
||||||
|
static async getQuestionsByRepo(repoId: string): Promise<ApiResponse<Question[]>> {
|
||||||
|
console.log('🚀 查询题库下题目:', { repoId })
|
||||||
|
const response = await ApiRequest.get<Question[]>(`/biz/repo/questionList/${repoId}`)
|
||||||
|
console.log('✅ 查询题库下题目成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询题目详情
|
||||||
|
*/
|
||||||
|
static async getQuestionDetail(questionId: string): Promise<ApiResponse<Question>> {
|
||||||
|
console.log('🚀 查询题目详情:', { questionId })
|
||||||
|
const response = await ApiRequest.get<Question>(`/biz/repo/repoList/${questionId}`)
|
||||||
|
console.log('✅ 查询题目详情成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加题目
|
||||||
|
*/
|
||||||
|
static async createQuestion(data: CreateQuestionRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 添加题目:', data)
|
||||||
|
const response = await ApiRequest.post<string>('/gen/question/question/add', data)
|
||||||
|
console.log('✅ 添加题目成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑题目
|
||||||
|
*/
|
||||||
|
static async updateQuestion(data: UpdateQuestionRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 编辑题目:', data)
|
||||||
|
const response = await ApiRequest.put<string>('/gen/question/question/edit', data)
|
||||||
|
console.log('✅ 编辑题目成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除题目
|
||||||
|
*/
|
||||||
|
static async deleteQuestion(id: string): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 删除题目:', { id })
|
||||||
|
const response = await ApiRequest.delete<string>('/gen/question/question/delete', {
|
||||||
|
params: { id }
|
||||||
|
})
|
||||||
|
console.log('✅ 删除题目成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 题目选项管理 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加题目选项
|
||||||
|
*/
|
||||||
|
static async createQuestionOption(data: CreateQuestionOptionRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 添加题目选项:', data)
|
||||||
|
const response = await ApiRequest.post<string>('/gen/questionoption/questionOption/add', data)
|
||||||
|
console.log('✅ 添加题目选项成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑题目选项
|
||||||
|
*/
|
||||||
|
static async updateQuestionOption(data: UpdateQuestionOptionRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 编辑题目选项:', data)
|
||||||
|
const response = await ApiRequest.put<string>('/gen/questionoption/questionOption/edit', data)
|
||||||
|
console.log('✅ 编辑题目选项成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除题目选项
|
||||||
|
*/
|
||||||
|
static async deleteQuestionOption(id: string): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 删除题目选项:', { id })
|
||||||
|
const response = await ApiRequest.delete<string>('/gen/questionoption/questionOption/delete', {
|
||||||
|
params: { id }
|
||||||
|
})
|
||||||
|
console.log('✅ 删除题目选项成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 题目答案管理 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加题目答案
|
||||||
|
*/
|
||||||
|
static async createQuestionAnswer(data: CreateQuestionAnswerRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 添加题目答案:', data)
|
||||||
|
const response = await ApiRequest.post<string>('/gen/questionanswer/questionAnswer/add', data)
|
||||||
|
console.log('✅ 添加题目答案成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑题目答案
|
||||||
|
*/
|
||||||
|
static async updateQuestionAnswer(data: UpdateQuestionAnswerRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 编辑题目答案:', data)
|
||||||
|
const response = await ApiRequest.put<string>('/gen/questionanswer/questionAnswer/edit', data)
|
||||||
|
console.log('✅ 编辑题目答案成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除题目答案
|
||||||
|
*/
|
||||||
|
static async deleteQuestionAnswer(id: string): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 删除题目答案:', { id })
|
||||||
|
const response = await ApiRequest.delete<string>('/gen/questionanswer/questionAnswer/delete', {
|
||||||
|
params: { id }
|
||||||
|
})
|
||||||
|
console.log('✅ 删除题目答案成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 题库题目关联管理 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加题库题目关联
|
||||||
|
*/
|
||||||
|
static async createQuestionRepo(data: CreateQuestionRepoRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 添加题库题目关联:', data)
|
||||||
|
const response = await ApiRequest.post<string>('/gen/questionrepo/questionRepo/add', data)
|
||||||
|
console.log('✅ 添加题库题目关联成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑题库题目关联
|
||||||
|
*/
|
||||||
|
static async updateQuestionRepo(data: UpdateQuestionRepoRequest): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 编辑题库题目关联:', data)
|
||||||
|
const response = await ApiRequest.put<string>('/gen/questionrepo/questionRepo/edit', data)
|
||||||
|
console.log('✅ 编辑题库题目关联成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除题库题目关联
|
||||||
|
*/
|
||||||
|
static async deleteQuestionRepo(id: string): Promise<ApiResponse<string>> {
|
||||||
|
console.log('🚀 删除题库题目关联:', { id })
|
||||||
|
const response = await ApiRequest.delete<string>('/gen/questionrepo/questionRepo/delete', {
|
||||||
|
params: { id }
|
||||||
|
})
|
||||||
|
console.log('✅ 删除题库题目关联成功:', response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 常用工具方法 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题目类型映射
|
||||||
|
*/
|
||||||
|
static getQuestionTypeText(type: number): string {
|
||||||
|
const typeMap: Record<number, string> = {
|
||||||
|
0: '单选题',
|
||||||
|
1: '多选题',
|
||||||
|
2: '判断题',
|
||||||
|
3: '填空题',
|
||||||
|
4: '简答题',
|
||||||
|
5: '复合题'
|
||||||
|
}
|
||||||
|
return typeMap[type] || '未知类型'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 难度等级映射
|
||||||
|
*/
|
||||||
|
static getDifficultyText(difficulty: number): string {
|
||||||
|
const difficultyMap: Record<number, string> = {
|
||||||
|
1: '简单',
|
||||||
|
2: '中等',
|
||||||
|
3: '困难'
|
||||||
|
}
|
||||||
|
return difficultyMap[difficulty] || '未知难度'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加题目选项
|
||||||
|
*/
|
||||||
|
static async batchCreateQuestionOptions(
|
||||||
|
questionId: string,
|
||||||
|
options: Omit<CreateQuestionOptionRequest, 'questionId'>[]
|
||||||
|
): Promise<ApiResponse<string>[]> {
|
||||||
|
console.log('🚀 批量添加题目选项:', { questionId, options })
|
||||||
|
|
||||||
|
const promises = options.map(option =>
|
||||||
|
this.createQuestionOption({ ...option, questionId })
|
||||||
|
)
|
||||||
|
|
||||||
|
const responses = await Promise.all(promises)
|
||||||
|
console.log('✅ 批量添加题目选项成功:', responses)
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加题目答案
|
||||||
|
*/
|
||||||
|
static async batchCreateQuestionAnswers(
|
||||||
|
questionId: string,
|
||||||
|
answers: Omit<CreateQuestionAnswerRequest, 'questionId'>[]
|
||||||
|
): Promise<ApiResponse<string>[]> {
|
||||||
|
console.log('🚀 批量添加题目答案:', { questionId, answers })
|
||||||
|
|
||||||
|
const promises = answers.map(answer =>
|
||||||
|
this.createQuestionAnswer({ ...answer, questionId })
|
||||||
|
)
|
||||||
|
|
||||||
|
const responses = await Promise.all(promises)
|
||||||
|
console.log('✅ 批量添加题目答案成功:', responses)
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExamApi
|
131
src/api/types.ts
131
src/api/types.ts
@ -684,3 +684,134 @@ export interface Statistics {
|
|||||||
count: number
|
count: number
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 考试题库相关类型
|
||||||
|
export interface Repo {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
remark: string
|
||||||
|
questionCount?: number
|
||||||
|
createBy: string
|
||||||
|
createTime: string
|
||||||
|
updateBy: string
|
||||||
|
updateTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Question {
|
||||||
|
id: string
|
||||||
|
parentId: string
|
||||||
|
type: number // 0单选题 1多选题 2判断题 3填空题 4简答题 5复合题
|
||||||
|
content: string
|
||||||
|
analysis: string
|
||||||
|
difficulty: number
|
||||||
|
score: number
|
||||||
|
createBy: string
|
||||||
|
createTime: string
|
||||||
|
updateBy: string
|
||||||
|
updateTime: string
|
||||||
|
options?: QuestionOption[]
|
||||||
|
answers?: QuestionAnswer[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionOption {
|
||||||
|
id: string
|
||||||
|
questionId: string
|
||||||
|
content: string
|
||||||
|
izCorrent: number // 是否正确答案
|
||||||
|
orderNo: number
|
||||||
|
createBy: string
|
||||||
|
createTime: string
|
||||||
|
updateBy: string
|
||||||
|
updateTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionAnswer {
|
||||||
|
id: string
|
||||||
|
questionId: string
|
||||||
|
answerText: string
|
||||||
|
orderNo: number
|
||||||
|
createBy: string
|
||||||
|
createTime: string
|
||||||
|
updateBy: string
|
||||||
|
updateTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionRepo {
|
||||||
|
id: string
|
||||||
|
repoId: string
|
||||||
|
questionId: string
|
||||||
|
createBy: string
|
||||||
|
createTime: string
|
||||||
|
updateBy: string
|
||||||
|
updateTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 题库相关请求参数类型
|
||||||
|
export interface CreateRepoRequest {
|
||||||
|
title: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateRepoRequest {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateQuestionRequest {
|
||||||
|
parentId?: string
|
||||||
|
type: number
|
||||||
|
content: string
|
||||||
|
analysis?: string
|
||||||
|
difficulty: number
|
||||||
|
score: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateQuestionRequest {
|
||||||
|
id: string
|
||||||
|
parentId?: string
|
||||||
|
type: number
|
||||||
|
content: string
|
||||||
|
analysis?: string
|
||||||
|
difficulty: number
|
||||||
|
score: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateQuestionOptionRequest {
|
||||||
|
questionId: string
|
||||||
|
content: string
|
||||||
|
izCorrent: number
|
||||||
|
orderNo: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateQuestionOptionRequest {
|
||||||
|
id: string
|
||||||
|
questionId: string
|
||||||
|
content: string
|
||||||
|
izCorrent: number
|
||||||
|
orderNo: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateQuestionAnswerRequest {
|
||||||
|
questionId: string
|
||||||
|
answerText: string
|
||||||
|
orderNo: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateQuestionAnswerRequest {
|
||||||
|
id: string
|
||||||
|
questionId: string
|
||||||
|
answerText: string
|
||||||
|
orderNo: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateQuestionRepoRequest {
|
||||||
|
repoId: string
|
||||||
|
questionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateQuestionRepoRequest {
|
||||||
|
id: string
|
||||||
|
repoId: string
|
||||||
|
questionId: string
|
||||||
|
}
|
||||||
|
@ -12,24 +12,73 @@
|
|||||||
class="big-question-section">
|
class="big-question-section">
|
||||||
<div v-for="(subQuestion, subIndex) in bigQuestion.subQuestions" :key="subQuestion.id"
|
<div v-for="(subQuestion, subIndex) in bigQuestion.subQuestions" :key="subQuestion.id"
|
||||||
class="question-item">
|
class="question-item">
|
||||||
<div class="question-info">
|
<!-- 普通题目显示 -->
|
||||||
<span class="question-number">{{ bigIndex + 1 }}.{{ subIndex + 1 }}</span>
|
<template v-if="subQuestion.type !== 'composite'">
|
||||||
<div class="question-content">
|
<div class="question-info">
|
||||||
{{ subQuestion.title }}
|
<span class="question-number">{{ bigIndex + 1 }}.{{ subIndex + 1 }}</span>
|
||||||
|
<div class="question-content">
|
||||||
|
{{ subQuestion.title }}
|
||||||
|
</div>
|
||||||
|
<span class="question-type">{{ getQuestionTypeName(subQuestion.type) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="question-type">{{ getQuestionTypeName(subQuestion.type) }}</span>
|
<div class="question-score">
|
||||||
</div>
|
<span class="score-label">分数:</span>
|
||||||
<div class="question-score">
|
<n-input-number
|
||||||
<span class="score-label">分数:</span>
|
v-model:value="subQuestion.score"
|
||||||
<n-input-number
|
size="small"
|
||||||
v-model:value="subQuestion.score"
|
:min="0"
|
||||||
size="small"
|
:max="100"
|
||||||
:min="0"
|
:precision="1"
|
||||||
:max="100"
|
@update:value="updateQuestionScore(bigIndex, subIndex, $event)"
|
||||||
:precision="1"
|
/>
|
||||||
@update:value="updateQuestionScore(bigIndex, subIndex, $event)"
|
</div>
|
||||||
/>
|
</template>
|
||||||
</div>
|
|
||||||
|
<!-- 复合题目显示 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="composite-question-container">
|
||||||
|
<!-- 复合题主体 -->
|
||||||
|
<div class="composite-main">
|
||||||
|
<div class="question-info">
|
||||||
|
<span class="question-number composite">{{ bigIndex + 1 }}.{{ subIndex + 1 }}</span>
|
||||||
|
<div class="question-content">
|
||||||
|
{{ subQuestion.title }}
|
||||||
|
</div>
|
||||||
|
<span class="question-type composite">{{ getQuestionTypeName(subQuestion.type) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="question-score">
|
||||||
|
<span class="score-label">总分:</span>
|
||||||
|
<span class="total-score">{{ getCompositeQuestionTotalScore(subQuestion) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复合题的子题目 -->
|
||||||
|
<div class="composite-sub-questions">
|
||||||
|
<div v-for="(compositeSubQ, compositeIndex) in (subQuestion.subQuestions || [])"
|
||||||
|
:key="compositeSubQ.id"
|
||||||
|
class="composite-sub-item">
|
||||||
|
<div class="question-info sub">
|
||||||
|
<span class="question-number sub">{{ bigIndex + 1 }}.{{ subIndex + 1 }}.{{ compositeIndex + 1 }}</span>
|
||||||
|
<div class="question-content sub">
|
||||||
|
{{ compositeSubQ.title }}
|
||||||
|
</div>
|
||||||
|
<span class="question-type sub">{{ getQuestionTypeName(compositeSubQ.type) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="question-score">
|
||||||
|
<span class="score-label">分数:</span>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="compositeSubQ.score"
|
||||||
|
size="small"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:precision="1"
|
||||||
|
@update:value="updateCompositeSubQuestionScore(bigIndex, subIndex, compositeIndex, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -159,6 +208,33 @@ const updateQuestionScore = (bigIndex: number, subIndex: number, score: number)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取复合题总分
|
||||||
|
const getCompositeQuestionTotalScore = (subQuestion: SubQuestion): number => {
|
||||||
|
if (subQuestion.type === QuestionType.COMPOSITE && subQuestion.subQuestions) {
|
||||||
|
return subQuestion.subQuestions.reduce((total, sq) => total + (sq.score || 0), 0);
|
||||||
|
}
|
||||||
|
return subQuestion.score || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新复合题子题目分数
|
||||||
|
const updateCompositeSubQuestionScore = (bigIndex: number, subIndex: number, compositeIndex: number, score: number) => {
|
||||||
|
const bigQuestion = questionList.value[bigIndex];
|
||||||
|
const subQuestion = bigQuestion?.subQuestions[subIndex];
|
||||||
|
|
||||||
|
if (subQuestion && subQuestion.type === QuestionType.COMPOSITE && subQuestion.subQuestions) {
|
||||||
|
const compositeSubQ = subQuestion.subQuestions[compositeIndex];
|
||||||
|
if (compositeSubQ) {
|
||||||
|
compositeSubQ.score = score || 0;
|
||||||
|
|
||||||
|
// 重新计算复合题总分
|
||||||
|
subQuestion.score = subQuestion.subQuestions.reduce((total, sq) => total + (sq.score || 0), 0);
|
||||||
|
|
||||||
|
// 重新计算大题总分
|
||||||
|
bigQuestion.totalScore = bigQuestion.subQuestions.reduce((total, sub) => total + (sub.score || 0), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 取消批量设置
|
// 取消批量设置
|
||||||
const cancelBatchSet = () => {
|
const cancelBatchSet = () => {
|
||||||
showModal.value = false;
|
showModal.value = false;
|
||||||
@ -231,6 +307,41 @@ const confirmBatchSet = () => {
|
|||||||
border-color: #d1e7dd;
|
border-color: #d1e7dd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composite-question {
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
border-left: 3px solid #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composite-sub-question {
|
||||||
|
margin: 8px 0 8px 20px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d0d0d0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composite-sub-question::before {
|
||||||
|
content: "└";
|
||||||
|
position: absolute;
|
||||||
|
left: -15px;
|
||||||
|
top: 8px;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composite-sub-header {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-score {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
.question-info {
|
.question-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -21,6 +21,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 考试人数限制 -->
|
||||||
|
<div class="setting-row">
|
||||||
|
<label class="setting-label">考试人数</label>
|
||||||
|
<n-input v-model:value="formData.maxParticipants" type="number" :min="1" placeholder="请输入考试人数上限" class="setting-input" />
|
||||||
|
</div>
|
||||||
<!-- 试卷分类 -->
|
<!-- 试卷分类 -->
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<label class="setting-label">试卷分类</label>
|
<label class="setting-label">试卷分类</label>
|
||||||
@ -323,6 +328,8 @@ interface ExamSettings {
|
|||||||
useLastScore: boolean; // 最后一次练习成绩为最终成绩
|
useLastScore: boolean; // 最后一次练习成绩为最终成绩
|
||||||
};
|
};
|
||||||
paperMode: 'show_all' | 'show_current' | 'hide_all';
|
paperMode: 'show_all' | 'show_current' | 'hide_all';
|
||||||
|
// 新增考试人数限制字段
|
||||||
|
maxParticipants: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Props 定义
|
// Props 定义
|
||||||
@ -396,6 +403,8 @@ const formData = ref<ExamSettings>({
|
|||||||
useLastScore: false,
|
useLastScore: false,
|
||||||
},
|
},
|
||||||
paperMode: 'show_all',
|
paperMode: 'show_all',
|
||||||
|
// 新增考试人数限制字段
|
||||||
|
maxParticipants: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听 props.examData 变化,更新本地副本
|
// 监听 props.examData 变化,更新本地副本
|
||||||
|
@ -166,7 +166,10 @@ let nextId = 1
|
|||||||
// 监听props变化
|
// 监听props变化
|
||||||
watch(() => props.modelValue, (newValue) => {
|
watch(() => props.modelValue, (newValue) => {
|
||||||
if (newValue?.subQuestions) {
|
if (newValue?.subQuestions) {
|
||||||
subQuestions.value = newValue.subQuestions
|
subQuestions.value = newValue.subQuestions.map(sq => ({ ...sq }));
|
||||||
|
console.log('CompositeQuestion props 更新:', {
|
||||||
|
newSubQuestions: subQuestions.value.map(sq => ({ id: sq.id, title: sq.title, score: sq.score }))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
@ -85,6 +85,7 @@ import {
|
|||||||
NTimeline,
|
NTimeline,
|
||||||
NTimelineItem,
|
NTimelineItem,
|
||||||
NMessageProvider,
|
NMessageProvider,
|
||||||
|
NDialogProvider,
|
||||||
NPopselect
|
NPopselect
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
|
|
||||||
@ -159,6 +160,7 @@ const naive = create({
|
|||||||
NTimeline,
|
NTimeline,
|
||||||
NTimelineItem,
|
NTimelineItem,
|
||||||
NMessageProvider,
|
NMessageProvider,
|
||||||
|
NDialogProvider,
|
||||||
NPopselect
|
NPopselect
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -62,6 +62,7 @@ import HomeworkTemplateImport from '@/views/teacher/course/HomeworkTemplateImpor
|
|||||||
|
|
||||||
// 考试管理组件
|
// 考试管理组件
|
||||||
import ExamManagement from '@/views/teacher/ExamPages/ExamPage.vue'
|
import ExamManagement from '@/views/teacher/ExamPages/ExamPage.vue'
|
||||||
|
import ExamQuestionBankManagement from '@/views/teacher/ExamPages/QuestionBankManagement.vue'
|
||||||
import QuestionManagement from '@/views/teacher/ExamPages/QuestionManagement.vue'
|
import QuestionManagement from '@/views/teacher/ExamPages/QuestionManagement.vue'
|
||||||
import ExamLibrary from '@/views/teacher/ExamPages/ExamLibrary.vue'
|
import ExamLibrary from '@/views/teacher/ExamPages/ExamLibrary.vue'
|
||||||
import MarkingCenter from '@/views/teacher/ExamPages/MarkingCenter.vue'
|
import MarkingCenter from '@/views/teacher/ExamPages/MarkingCenter.vue'
|
||||||
@ -69,6 +70,7 @@ import AddExam from '@/views/teacher/ExamPages/AddExam.vue'
|
|||||||
import AddQuestion from '@/views/teacher/ExamPages/AddQuestion.vue'
|
import AddQuestion from '@/views/teacher/ExamPages/AddQuestion.vue'
|
||||||
import StudentList from '@/views/teacher/ExamPages/StudentList.vue'
|
import StudentList from '@/views/teacher/ExamPages/StudentList.vue'
|
||||||
import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue'
|
import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue'
|
||||||
|
import ExamTaking from '@/views/teacher/ExamPages/ExamTaking.vue'
|
||||||
|
|
||||||
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
|
import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue'
|
||||||
|
|
||||||
@ -293,10 +295,16 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'ExamManagement',
|
name: 'ExamManagement',
|
||||||
component: ExamManagement,
|
component: ExamManagement,
|
||||||
meta: { title: '考试管理' },
|
meta: { title: '考试管理' },
|
||||||
redirect: '/teacher/exam-management/question-management',
|
redirect: '/teacher/exam-management/question-bank',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'question-management',
|
path: 'question-bank',
|
||||||
|
name: 'ExamQuestionBankManagement',
|
||||||
|
component: ExamQuestionBankManagement,
|
||||||
|
meta: { title: '题库管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'question-bank/:bankId/questions',
|
||||||
name: 'QuestionManagement',
|
name: 'QuestionManagement',
|
||||||
component: QuestionManagement,
|
component: QuestionManagement,
|
||||||
meta: { title: '试题管理' }
|
meta: { title: '试题管理' }
|
||||||
@ -347,7 +355,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { title: '试卷预览' }
|
meta: { title: '试卷预览' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'add-question/:id?',
|
path: 'add-question/:id/:questionId?',
|
||||||
name: 'AddQuestionPage',
|
name: 'AddQuestionPage',
|
||||||
component: AddQuestion,
|
component: AddQuestion,
|
||||||
meta: { title: '添加试题' }
|
meta: { title: '添加试题' }
|
||||||
@ -357,7 +365,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/taking/:id',
|
||||||
|
name: 'ExamTaking',
|
||||||
|
component: ExamTaking,
|
||||||
|
meta: { title: '考试进行中' }
|
||||||
|
},
|
||||||
|
|
||||||
// 帮助中心
|
// 帮助中心
|
||||||
{
|
{
|
||||||
|
@ -36,10 +36,10 @@
|
|||||||
|
|
||||||
<!-- 考试管理子菜单 -->
|
<!-- 考试管理子菜单 -->
|
||||||
<div class="submenu-container" :class="{ expanded: examMenuExpanded }">
|
<div class="submenu-container" :class="{ expanded: examMenuExpanded }">
|
||||||
<router-link to="/teacher/exam-management/question-management" class="submenu-item"
|
<router-link to="/teacher/exam-management/question-bank" class="submenu-item"
|
||||||
:class="{ active: activeSubNavItem === 'question-management' }"
|
:class="{ active: activeSubNavItem === 'question-bank' }"
|
||||||
@click="setActiveSubNavItem('question-management')">
|
@click="setActiveSubNavItem('question-bank')">
|
||||||
<span>试题管理</span>
|
<span>题库管理</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/teacher/exam-management/exam-library" class="submenu-item"
|
<router-link to="/teacher/exam-management/exam-library" class="submenu-item"
|
||||||
:class="{ active: activeSubNavItem === 'exam-library' }" @click="setActiveSubNavItem('exam-library')">
|
:class="{ active: activeSubNavItem === 'exam-library' }" @click="setActiveSubNavItem('exam-library')">
|
||||||
@ -505,8 +505,8 @@ const updateActiveNavItem = () => {
|
|||||||
} else if (path.includes('exam-management')) {
|
} else if (path.includes('exam-management')) {
|
||||||
activeNavItem.value = 4; // 考试管理
|
activeNavItem.value = 4; // 考试管理
|
||||||
examMenuExpanded.value = true;
|
examMenuExpanded.value = true;
|
||||||
|
|
||||||
const arr = ['question-management', 'exam-library', 'marking-center'];
|
const arr = ['question-bank', 'exam-library', 'marking-center'];
|
||||||
const found = arr.find(item => path.includes(item));
|
const found = arr.find(item => path.includes(item));
|
||||||
activeSubNavItem.value = found || '';
|
activeSubNavItem.value = found || '';
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -252,6 +252,7 @@ import { useRouter, useRoute } from 'vue-router';
|
|||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { ArrowBackOutline } from '@vicons/ionicons5';
|
import { ArrowBackOutline } from '@vicons/ionicons5';
|
||||||
import QuestionTypeContainer from '@/components/teacher/QuestionTypeContainer.vue';
|
import QuestionTypeContainer from '@/components/teacher/QuestionTypeContainer.vue';
|
||||||
|
import { ExamApi } from '@/api';
|
||||||
|
|
||||||
// 路由和消息
|
// 路由和消息
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -259,9 +260,10 @@ const route = useRoute();
|
|||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
// 编辑模式判断
|
// 编辑模式判断
|
||||||
const questionId = route.params.id as string | undefined;
|
const questionId = route.params.questionId as string | undefined;
|
||||||
const isEditMode = ref(!!questionId);
|
const isEditMode = ref(!!questionId);
|
||||||
|
|
||||||
|
|
||||||
// 表单引用
|
// 表单引用
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@ -382,6 +384,29 @@ const goBack = () => {
|
|||||||
router.back();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 类型映射函数:将字符串类型转换为数字类型
|
||||||
|
const getQuestionTypeNumber = (type: string): number => {
|
||||||
|
const typeMap: Record<string, number> = {
|
||||||
|
'single_choice': 0, // 单选题
|
||||||
|
'multiple_choice': 1, // 多选题
|
||||||
|
'true_false': 2, // 判断题
|
||||||
|
'fill_blank': 3, // 填空题
|
||||||
|
'short_answer': 4, // 简答题
|
||||||
|
'composite': 5 // 复合题
|
||||||
|
};
|
||||||
|
return typeMap[type] || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 难度映射函数:将字符串难度转换为数字
|
||||||
|
const getDifficultyNumber = (difficulty: string): number => {
|
||||||
|
const difficultyMap: Record<string, number> = {
|
||||||
|
'easy': 1, // 简单
|
||||||
|
'medium': 2, // 中等
|
||||||
|
'hard': 3 // 困难
|
||||||
|
};
|
||||||
|
return difficultyMap[difficulty] || 1;
|
||||||
|
};
|
||||||
|
|
||||||
// 保存试题
|
// 保存试题
|
||||||
const saveQuestion = async () => {
|
const saveQuestion = async () => {
|
||||||
try {
|
try {
|
||||||
@ -395,22 +420,33 @@ const saveQuestion = async () => {
|
|||||||
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
|
||||||
// 构建保存数据
|
// 获取题库ID(可能从路由参数或者查询参数获取)
|
||||||
const saveData = buildSaveData();
|
// 如果从题库管理页面跳转过来,应该有bankId或者通过其他方式传递
|
||||||
|
let bankId = route.params.bankId as string || route.params.id as string || route.query.bankId as string;
|
||||||
// 模拟保存API - 根据编辑模式调用不同接口
|
|
||||||
if (isEditMode.value && questionId) {
|
if (!bankId) {
|
||||||
await mockUpdateQuestion(questionId, saveData);
|
// 尝试从浏览器历史记录或者本地存储获取
|
||||||
console.log('更新试题数据:', saveData);
|
const referrer = document.referrer;
|
||||||
message.success('试题更新成功');
|
const bankIdMatch = referrer.match(/question-bank\/([^\/]+)\/questions/);
|
||||||
} else {
|
if (bankIdMatch) {
|
||||||
await mockCreateQuestion(saveData);
|
bankId = bankIdMatch[1];
|
||||||
console.log('保存试题数据:', saveData);
|
}
|
||||||
message.success('试题保存成功');
|
}
|
||||||
|
|
||||||
|
if (!bankId) {
|
||||||
|
message.error('缺少题库ID参数,请从题库管理页面进入');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回试题管理页面
|
// 根据编辑模式调用不同接口
|
||||||
router.push('/teacher/exam-management/question-management');
|
if (isEditMode.value && questionId) {
|
||||||
|
// TODO: 实现编辑模式的接口调用
|
||||||
|
message.info('编辑模式暂未实现');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 新增模式 - 按照接口调用顺序执行
|
||||||
|
await createNewQuestion(bankId);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('保存试题失败:', error);
|
console.error('保存试题失败:', error);
|
||||||
@ -439,6 +475,99 @@ const saveQuestion = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 创建新题目的完整流程
|
||||||
|
const createNewQuestion = async (bankId: string) => {
|
||||||
|
try {
|
||||||
|
// 第一步:添加题干,获取questionId
|
||||||
|
const questionData = {
|
||||||
|
type: getQuestionTypeNumber(questionForm.type),
|
||||||
|
content: questionForm.title,
|
||||||
|
analysis: questionForm.explanation || '',
|
||||||
|
difficulty: getDifficultyNumber(questionForm.difficulty),
|
||||||
|
score: questionForm.score
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🚀 第一步:创建题目题干:', questionData);
|
||||||
|
const questionResponse = await ExamApi.createQuestion(questionData);
|
||||||
|
|
||||||
|
if (!questionResponse.data) {
|
||||||
|
throw new Error('创建题目失败,未返回题目ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdQuestionId = questionResponse.data;
|
||||||
|
console.log('✅ 题目创建成功,questionId:', createdQuestionId);
|
||||||
|
|
||||||
|
// 第二步:根据题型添加选项和答案
|
||||||
|
if (questionForm.type === 'single_choice') {
|
||||||
|
await handleSingleChoiceQuestion(createdQuestionId);
|
||||||
|
}
|
||||||
|
// TODO: 处理其他题型
|
||||||
|
// else if (questionForm.type === 'multiple_choice') {
|
||||||
|
// await handleMultipleChoiceQuestion(createdQuestionId);
|
||||||
|
// } else if (questionForm.type === 'true_false') {
|
||||||
|
// await handleTrueFalseQuestion(createdQuestionId);
|
||||||
|
// }
|
||||||
|
// 其他题型暂不处理
|
||||||
|
|
||||||
|
// 最后一步:将题目绑定到题库
|
||||||
|
const questionRepoData = {
|
||||||
|
repoId: bankId,
|
||||||
|
questionId: createdQuestionId
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🚀 最后一步:绑定题目到题库:', questionRepoData);
|
||||||
|
await ExamApi.createQuestionRepo(questionRepoData);
|
||||||
|
console.log('✅ 题目绑定到题库成功');
|
||||||
|
|
||||||
|
message.success('题目保存成功');
|
||||||
|
|
||||||
|
// 返回题库管理页面
|
||||||
|
router.push(`/teacher/exam-management/question-bank/${bankId}/questions`);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('创建题目流程失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理单选题的选项和答案
|
||||||
|
const handleSingleChoiceQuestion = async (questionId: string) => {
|
||||||
|
try {
|
||||||
|
// 添加选项
|
||||||
|
const optionPromises = questionForm.options.map((option, index) => {
|
||||||
|
const isCorrect = questionForm.correctAnswer === index ? 1 : 0;
|
||||||
|
return ExamApi.createQuestionOption({
|
||||||
|
questionId,
|
||||||
|
content: option.content,
|
||||||
|
izCorrent: isCorrect,
|
||||||
|
orderNo: index + 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🚀 第二步:添加选项,选项数量:', questionForm.options.length);
|
||||||
|
await Promise.all(optionPromises);
|
||||||
|
console.log('✅ 选项添加成功');
|
||||||
|
|
||||||
|
// 添加正确答案
|
||||||
|
if (questionForm.correctAnswer !== null) {
|
||||||
|
const correctOption = questionForm.options[questionForm.correctAnswer];
|
||||||
|
const answerData = {
|
||||||
|
questionId,
|
||||||
|
answerText: correctOption.content,
|
||||||
|
orderNo: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🚀 第三步:添加答案:', answerData);
|
||||||
|
await ExamApi.createQuestionAnswer(answerData);
|
||||||
|
console.log('✅ 答案添加成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('处理单选题失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 验证答案设置
|
// 验证答案设置
|
||||||
const validateAnswers = (): boolean => {
|
const validateAnswers = (): boolean => {
|
||||||
console.log(questionForm);
|
console.log(questionForm);
|
||||||
@ -449,12 +578,30 @@ const validateAnswers = (): boolean => {
|
|||||||
|
|
||||||
switch (questionForm.type) {
|
switch (questionForm.type) {
|
||||||
case 'single_choice':
|
case 'single_choice':
|
||||||
|
// 检查选项内容
|
||||||
|
if (questionForm.options.length < 2) {
|
||||||
|
message.error('单选题至少需要2个选项');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (questionForm.options.some(option => !option.content.trim())) {
|
||||||
|
message.error('请填写所有选项的内容');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (questionForm.correctAnswer === null) {
|
if (questionForm.correctAnswer === null) {
|
||||||
message.error('请设置单选题的正确答案');
|
message.error('请设置单选题的正确答案');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'multiple_choice':
|
case 'multiple_choice':
|
||||||
|
// 检查选项内容
|
||||||
|
if (questionForm.options.length < 2) {
|
||||||
|
message.error('多选题至少需要2个选项');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (questionForm.options.some(option => !option.content.trim())) {
|
||||||
|
message.error('请填写所有选项的内容');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (questionForm.correctAnswers.length === 0) {
|
if (questionForm.correctAnswers.length === 0) {
|
||||||
message.error('请设置多选题的正确答案');
|
message.error('请设置多选题的正确答案');
|
||||||
return false;
|
return false;
|
||||||
@ -623,51 +770,6 @@ const validateCompositeQuestion = (): boolean => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 构建保存数据
|
|
||||||
const buildSaveData = () => {
|
|
||||||
const baseData = {
|
|
||||||
type: questionForm.type,
|
|
||||||
category: questionForm.category,
|
|
||||||
difficulty: questionForm.difficulty,
|
|
||||||
score: questionForm.score,
|
|
||||||
title: questionForm.title,
|
|
||||||
explanation: questionForm.explanation
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (questionForm.type) {
|
|
||||||
case 'single_choice':
|
|
||||||
case 'multiple_choice':
|
|
||||||
return {
|
|
||||||
...baseData,
|
|
||||||
options: questionForm.options,
|
|
||||||
correctAnswer: questionForm.type === 'single_choice' ? questionForm.correctAnswer : undefined,
|
|
||||||
correctAnswers: questionForm.type === 'multiple_choice' ? questionForm.correctAnswers : undefined
|
|
||||||
};
|
|
||||||
case 'true_false':
|
|
||||||
return {
|
|
||||||
...baseData,
|
|
||||||
answer: questionForm.trueFalseAnswer
|
|
||||||
};
|
|
||||||
case 'fill_blank':
|
|
||||||
return {
|
|
||||||
...baseData,
|
|
||||||
answers: questionForm.fillBlankAnswers.filter(answer => answer.content.trim())
|
|
||||||
};
|
|
||||||
case 'short_answer':
|
|
||||||
return {
|
|
||||||
...baseData,
|
|
||||||
answer: questionForm.shortAnswer
|
|
||||||
};
|
|
||||||
case 'composite':
|
|
||||||
return {
|
|
||||||
...baseData,
|
|
||||||
compositeData: questionForm.compositeData
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return baseData;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件挂载
|
// 组件挂载
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 如果是编辑模式,加载题目数据
|
// 如果是编辑模式,加载题目数据
|
||||||
@ -676,164 +778,22 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载题目数据
|
// 加载题目数据(编辑模式使用)
|
||||||
const loadQuestionData = async (id: string) => {
|
const loadQuestionData = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
// 模拟API调用
|
// TODO: 实现编辑模式的数据加载
|
||||||
const response = await mockGetQuestionById(id);
|
// const response = await ExamApi.getQuestionDetail(id);
|
||||||
|
// if (response.data) {
|
||||||
if (response.success && response.data) {
|
// // 根据返回的数据回显表单
|
||||||
const questionData = response.data as any; // 使用any类型避免复杂的类型定义
|
// }
|
||||||
|
console.log('加载题目数据,题目ID:', id);
|
||||||
// 回显基本信息
|
message.info('编辑模式数据加载暂未实现');
|
||||||
questionForm.type = questionData.type;
|
|
||||||
questionForm.category = questionData.category;
|
|
||||||
questionForm.difficulty = questionData.difficulty;
|
|
||||||
questionForm.score = questionData.score;
|
|
||||||
questionForm.title = questionData.title;
|
|
||||||
questionForm.explanation = questionData.explanation || '';
|
|
||||||
|
|
||||||
// 根据题型回显具体数据
|
|
||||||
switch (questionData.type) {
|
|
||||||
case 'single_choice':
|
|
||||||
questionForm.options = questionData.options || [];
|
|
||||||
questionForm.correctAnswer = questionData.correctAnswer;
|
|
||||||
break;
|
|
||||||
case 'multiple_choice':
|
|
||||||
questionForm.options = questionData.options || [];
|
|
||||||
questionForm.correctAnswers = questionData.correctAnswers || [];
|
|
||||||
break;
|
|
||||||
case 'true_false':
|
|
||||||
questionForm.trueFalseAnswer = questionData.answer;
|
|
||||||
break;
|
|
||||||
case 'fill_blank':
|
|
||||||
questionForm.fillBlankAnswers = questionData.answers || [];
|
|
||||||
break;
|
|
||||||
case 'short_answer':
|
|
||||||
questionForm.shortAnswer = questionData.answer || '';
|
|
||||||
break;
|
|
||||||
case 'composite':
|
|
||||||
questionForm.compositeData = questionData.compositeData || { subQuestions: [] };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.success('题目数据加载成功');
|
|
||||||
} else {
|
|
||||||
message.error('加载题目数据失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载题目数据错误:', error);
|
console.error('加载题目数据错误:', error);
|
||||||
message.error('加载题目数据失败,请检查网络连接');
|
message.error('加载题目数据失败,请检查网络连接');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模拟获取题目数据的API
|
|
||||||
const mockGetQuestionById = async (id: string) => {
|
|
||||||
// 模拟API延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// 模拟返回数据
|
|
||||||
const mockQuestions = {
|
|
||||||
'1': {
|
|
||||||
type: 'single_choice',
|
|
||||||
category: 'exam',
|
|
||||||
difficulty: 'medium',
|
|
||||||
score: 5,
|
|
||||||
title: '以下哪个是Vue.js的核心特性?',
|
|
||||||
explanation: 'Vue.js的核心特性包括响应式数据绑定、组件系统等',
|
|
||||||
options: [
|
|
||||||
{ content: '响应式数据绑定' },
|
|
||||||
{ content: '虚拟DOM' },
|
|
||||||
{ content: '组件系统' },
|
|
||||||
{ content: '以上都是' }
|
|
||||||
],
|
|
||||||
correctAnswer: 3
|
|
||||||
},
|
|
||||||
'2': {
|
|
||||||
type: 'composite',
|
|
||||||
category: 'exam',
|
|
||||||
difficulty: 'hard',
|
|
||||||
score: 20,
|
|
||||||
title: '阅读以下关于JavaScript的材料,并回答相关问题。\\n\\nJavaScript是一种高级的、解释型的编程语言,具有动态类型、原型继承等特性。它最初被设计用于网页开发,但现在已经扩展到服务器端、移动应用等多个领域。',
|
|
||||||
explanation: '这是一道综合性的复合题',
|
|
||||||
compositeData: {
|
|
||||||
subQuestions: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
type: 'single',
|
|
||||||
title: 'JavaScript属于什么类型的编程语言?',
|
|
||||||
score: 5,
|
|
||||||
data: [
|
|
||||||
{ option: '编译型', id: 1 },
|
|
||||||
{ option: '解释型', id: 2 },
|
|
||||||
{ option: '汇编型', id: 3 },
|
|
||||||
{ option: '机器型', id: 4 }
|
|
||||||
],
|
|
||||||
correctAnswer: 1,
|
|
||||||
explanation: 'JavaScript是解释型编程语言'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
type: 'multiple',
|
|
||||||
title: 'JavaScript有哪些特性?',
|
|
||||||
score: 10,
|
|
||||||
data: [
|
|
||||||
{ option: '动态类型', id: 1 },
|
|
||||||
{ option: '原型继承', id: 2 },
|
|
||||||
{ option: '静态类型', id: 3 },
|
|
||||||
{ option: '面向对象', id: 4 }
|
|
||||||
],
|
|
||||||
correctAnswers: [0, 1, 3],
|
|
||||||
explanation: 'JavaScript具有动态类型、原型继承和面向对象等特性'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const question = mockQuestions[id as keyof typeof mockQuestions];
|
|
||||||
|
|
||||||
if (question) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: question
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: '题目不存在'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 模拟创建题目的API
|
|
||||||
const mockCreateQuestion = async (data: any) => {
|
|
||||||
// 模拟API延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// 这里应该调用真实的API
|
|
||||||
console.log('创建题目:', data);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: { id: Math.random().toString(36).substr(2, 9) }
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// 模拟更新题目的API
|
|
||||||
const mockUpdateQuestion = async (id: string, data: any) => {
|
|
||||||
// 模拟API延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// 这里应该调用真实的API
|
|
||||||
console.log('更新题目:', id, data);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: { id }
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const getDifficultyLabel = (difficulty: string): string => {
|
const getDifficultyLabel = (difficulty: string): string => {
|
||||||
const difficultyMap: { [key: string]: string } = {
|
const difficultyMap: { [key: string]: string } = {
|
||||||
|
@ -168,9 +168,6 @@ const paginationConfig = computed(() => ({
|
|||||||
pageSizes: [10, 20, 50, 100],
|
pageSizes: [10, 20, 50, 100],
|
||||||
showSizePicker: true,
|
showSizePicker: true,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
goto: ()=>{
|
|
||||||
return '跳转'
|
|
||||||
},
|
|
||||||
prefix: (info: { itemCount?: number }) => {
|
prefix: (info: { itemCount?: number }) => {
|
||||||
const itemCount = info.itemCount || 0;
|
const itemCount = info.itemCount || 0;
|
||||||
return `共 ${itemCount} 条`;
|
return `共 ${itemCount} 条`;
|
||||||
|
@ -64,7 +64,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="question-title">{{ subQuestion.title }}</div>
|
<div class="question-title">{{ subQuestion.title }}</div>
|
||||||
|
|
||||||
<!-- 单选题 -->
|
<!-- 单选题 -->
|
||||||
<div v-if="subQuestion.type === 'single_choice'" class="question-content">
|
<div v-if="subQuestion.type === 'single_choice'" class="question-content">
|
||||||
<div class="options">
|
<div class="options">
|
||||||
@ -116,13 +115,13 @@
|
|||||||
<div class="true-false-options">
|
<div class="true-false-options">
|
||||||
<div class="option-item"
|
<div class="option-item"
|
||||||
:class="{ 'correct-option': subQuestion.trueFalseAnswer === true }">
|
:class="{ 'correct-option': subQuestion.trueFalseAnswer === true }">
|
||||||
<span class="option-label">A</span>
|
<span class="option-label"></span>
|
||||||
<span class="option-content">正确</span>
|
<span class="option-content">正确</span>
|
||||||
<span v-if="subQuestion.trueFalseAnswer === true" class="correct-mark">✓</span>
|
<span v-if="subQuestion.trueFalseAnswer === true" class="correct-mark">✓</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="option-item"
|
<div class="option-item"
|
||||||
:class="{ 'correct-option': subQuestion.trueFalseAnswer === false }">
|
:class="{ 'correct-option': subQuestion.trueFalseAnswer === false }">
|
||||||
<span class="option-label">B</span>
|
<span class="option-label"></span>
|
||||||
<span class="option-content">错误</span>
|
<span class="option-content">错误</span>
|
||||||
<span v-if="subQuestion.trueFalseAnswer === false" class="correct-mark">✓</span>
|
<span v-if="subQuestion.trueFalseAnswer === false" class="correct-mark">✓</span>
|
||||||
</div>
|
</div>
|
||||||
@ -131,7 +130,7 @@
|
|||||||
<div class="answer-analysis">
|
<div class="answer-analysis">
|
||||||
<div class="correct-answer">
|
<div class="correct-answer">
|
||||||
<span class="label">正确答案:</span>
|
<span class="label">正确答案:</span>
|
||||||
<span class="answer">{{ subQuestion.trueFalseAnswer ? 'A 正确' : 'B 错误' }}</span>
|
<span class="answer">{{ subQuestion.trueFalseAnswer === undefined ? '未设置答案' : (subQuestion.trueFalseAnswer === true ? '正确' : '错误') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="subQuestion.explanation" class="explanation">
|
<div v-if="subQuestion.explanation" class="explanation">
|
||||||
<span class="label">答案解析:</span>
|
<span class="label">答案解析:</span>
|
||||||
@ -182,6 +181,147 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 复合题 -->
|
||||||
|
<div v-if="subQuestion.type === 'composite'" class="question-content">
|
||||||
|
<div class="composite-questions">
|
||||||
|
<!-- 复合题的子题目循环 -->
|
||||||
|
<div v-for="(compSubQ, compIndex) in subQuestion.subQuestions" :key="compSubQ.id" class="composite-sub-question">
|
||||||
|
<div class="comp-question-header">
|
||||||
|
<span class="comp-question-number">{{ compIndex + 1 }}. {{ getQuestionTypeName(compSubQ.type) }}</span>
|
||||||
|
<span class="comp-question-score">{{ compSubQ.score }}分</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="comp-question-title">{{ compSubQ.title }}</div>
|
||||||
|
|
||||||
|
<!-- 复合题中的单选题 -->
|
||||||
|
<div v-if="compSubQ.type === 'single_choice'" class="comp-question-content">
|
||||||
|
<div class="options">
|
||||||
|
<div v-for="(option, optIndex) in compSubQ.options" :key="option.id"
|
||||||
|
class="option-item" :class="{ 'correct-option': option.isCorrect }">
|
||||||
|
<span class="option-label">{{ String.fromCharCode(65 + optIndex) }}</span>
|
||||||
|
<span class="option-content">{{ option.content }}</span>
|
||||||
|
<span v-if="option.isCorrect" class="correct-mark">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">正确答案:</span>
|
||||||
|
<span class="answer">{{ getCorrectAnswerText(compSubQ) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="compSubQ.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ compSubQ.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复合题中的多选题 -->
|
||||||
|
<div v-if="compSubQ.type === 'multiple_choice'" class="comp-question-content">
|
||||||
|
<div class="options">
|
||||||
|
<div v-for="(option, optIndex) in compSubQ.options" :key="option.id"
|
||||||
|
class="option-item" :class="{ 'correct-option': option.isCorrect }">
|
||||||
|
<span class="option-label">{{ String.fromCharCode(65 + optIndex) }}</span>
|
||||||
|
<span class="option-content">{{ option.content }}</span>
|
||||||
|
<span v-if="option.isCorrect" class="correct-mark">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">正确答案:</span>
|
||||||
|
<span class="answer">{{ getCorrectAnswerText(compSubQ) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="compSubQ.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ compSubQ.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复合题中的判断题 -->
|
||||||
|
<div v-if="compSubQ.type === 'true_false'" class="comp-question-content">
|
||||||
|
<div class="true-false-options">
|
||||||
|
<div class="option-item"
|
||||||
|
:class="{ 'correct-option': compSubQ.trueFalseAnswer === true }">
|
||||||
|
<span class="option-label"></span>
|
||||||
|
<span class="option-content">正确</span>
|
||||||
|
<span v-if="compSubQ.trueFalseAnswer === true" class="correct-mark">✓</span>
|
||||||
|
</div>
|
||||||
|
<div class="option-item"
|
||||||
|
:class="{ 'correct-option': compSubQ.trueFalseAnswer === false }">
|
||||||
|
<span class="option-label"></span>
|
||||||
|
<span class="option-content">错误</span>
|
||||||
|
<span v-if="compSubQ.trueFalseAnswer === false" class="correct-mark">✓</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">正确答案:</span>
|
||||||
|
<span class="answer">{{ compSubQ.trueFalseAnswer === undefined ? '未设置答案' : (compSubQ.trueFalseAnswer === true ? '正确' : '错误') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="compSubQ.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ compSubQ.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复合题中的填空题 -->
|
||||||
|
<div v-if="compSubQ.type === 'fill_blank'" class="comp-question-content">
|
||||||
|
<div class="fill-blanks">
|
||||||
|
<div v-for="(blank,index) in compSubQ.fillBlanks" :key="blank.id" class="blank-item">
|
||||||
|
<span>第{{ index + 1 }}空:</span>
|
||||||
|
<div class="blank-answer">{{ blank.content || '未设置' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">正确答案:</span>
|
||||||
|
<div class="answer">
|
||||||
|
<div v-for="(blank,index) in compSubQ.fillBlanks" :key="blank.id">
|
||||||
|
第{{ index + 1 }}空:{{ blank.content || '未设置' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="compSubQ.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ compSubQ.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复合题中的简答题 -->
|
||||||
|
<div v-if="compSubQ.type === 'short_answer'" class="comp-question-content">
|
||||||
|
<div class="answer-area">
|
||||||
|
<div class="answer-text">{{ compSubQ.textAnswer || '未设置参考答案' }}</div>
|
||||||
|
</div>
|
||||||
|
<!-- 答案解析 -->
|
||||||
|
<div class="answer-analysis">
|
||||||
|
<div class="correct-answer">
|
||||||
|
<span class="label">参考答案:</span>
|
||||||
|
<p class="answer">{{ compSubQ.textAnswer || '未设置参考答案' }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="compSubQ.explanation" class="explanation">
|
||||||
|
<span class="label">答案解析:</span>
|
||||||
|
<p>{{ compSubQ.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 复合题整体解析 -->
|
||||||
|
<div v-if="subQuestion.explanation" class="answer-analysis">
|
||||||
|
<div class="explanation">
|
||||||
|
<span class="label">整体解析:</span>
|
||||||
|
<p>{{ subQuestion.explanation }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -658,6 +798,58 @@ onMounted(() => {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 复合题样式 */
|
||||||
|
.composite-questions {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composite-sub-question {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composite-sub-question:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comp-question-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comp-question-number {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comp-question-score {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comp-question-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comp-question-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.no-data {
|
.no-data {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
1104
src/views/teacher/ExamPages/ExamTaking.vue
Normal file
1104
src/views/teacher/ExamPages/ExamTaking.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -80,7 +80,7 @@
|
|||||||
<div class="tip-box ungraded"></div>未答
|
<div class="tip-box ungraded"></div>未答
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n-button type="primary" size="large" block @click="submitGrading">
|
<n-button v-if="!isViewMode" type="primary" size="large" block @click="submitGrading">
|
||||||
提交批阅
|
提交批阅
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
@ -176,6 +176,7 @@
|
|||||||
<div class="correct-status">
|
<div class="correct-status">
|
||||||
<label>对错:</label>
|
<label>对错:</label>
|
||||||
<n-radio-group v-model:value="question.isCorrect"
|
<n-radio-group v-model:value="question.isCorrect"
|
||||||
|
:disabled="isViewMode"
|
||||||
@update:value="(value: boolean | null) => updateQuestionStatus(question.id, value)">
|
@update:value="(value: boolean | null) => updateQuestionStatus(question.id, value)">
|
||||||
<n-radio :value="true" size="small">对</n-radio>
|
<n-radio :value="true" size="small">对</n-radio>
|
||||||
<n-radio :value="false" size="small">错</n-radio>
|
<n-radio :value="false" size="small">错</n-radio>
|
||||||
@ -187,6 +188,7 @@
|
|||||||
<div class="score-input-wrapper">
|
<div class="score-input-wrapper">
|
||||||
<n-input type="number" v-model:value="question.studentScore" :min="0"
|
<n-input type="number" v-model:value="question.studentScore" :min="0"
|
||||||
:max="question.score"
|
:max="question.score"
|
||||||
|
:disabled="isViewMode"
|
||||||
@update:value="(value: number | null) => updateQuestionScore(question.id, value)" />
|
@update:value="(value: number | null) => updateQuestionScore(question.id, value)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -197,8 +199,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<QuillEditor :placeholder="'请输入阅卷评语...(300字以内)'" v-model="gradingComments" height="400px">
|
<!-- 编辑模式:显示富文本编辑器 -->
|
||||||
</QuillEditor>
|
<div v-if="!isViewMode">
|
||||||
|
<QuillEditor :placeholder="'请输入阅卷评语...(300字以内)'" v-model="gradingComments" height="400px">
|
||||||
|
</QuillEditor>
|
||||||
|
</div>
|
||||||
|
<!-- 查看模式:显示评语内容 -->
|
||||||
|
<div v-else class="comments-display">
|
||||||
|
<h4>阅卷评语:</h4>
|
||||||
|
<div class="comments-content" v-if="gradingComments">
|
||||||
|
<div v-html="gradingComments"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-comments">
|
||||||
|
暂无评语
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -208,15 +223,19 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ArrowBackOutline, CheckmarkCircle, CloseCircle } from '@vicons/ionicons5'
|
import { ArrowBackOutline, CheckmarkCircle, CloseCircle } from '@vicons/ionicons5'
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import QuillEditor from '@/components/common/QuillEditor.vue'
|
import QuillEditor from '@/components/common/QuillEditor.vue'
|
||||||
|
|
||||||
// 路由
|
// 路由
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 根据路由参数判断是查看模式还是编辑模式
|
||||||
|
const isViewMode = computed(() => route.query.mode === 'view')
|
||||||
|
|
||||||
// 接口定义
|
// 接口定义
|
||||||
interface StudentInfo {
|
interface StudentInfo {
|
||||||
name: string
|
name: string
|
||||||
@ -506,6 +525,11 @@ onMounted(() => {
|
|||||||
if (questions.value.length > 0) {
|
if (questions.value.length > 0) {
|
||||||
currentQuestionId.value = questions.value[0].id
|
currentQuestionId.value = questions.value[0].id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是查看模式,加载已有的评语
|
||||||
|
if (isViewMode.value) {
|
||||||
|
// gradingComments.value = ''
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听筛选状态变化
|
// 监听筛选状态变化
|
||||||
@ -926,6 +950,7 @@ watch(showOnlyWrong, () => {
|
|||||||
.explanation-section h4 {
|
.explanation-section h4 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-answer,
|
.selected-answer,
|
||||||
@ -1026,6 +1051,36 @@ watch(showOnlyWrong, () => {
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 评语展示样式 */
|
||||||
|
.comments-display {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-display h4 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-content {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
min-height: 100px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-comments {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
648
src/views/teacher/ExamPages/QuestionBankManagement.vue
Normal file
648
src/views/teacher/ExamPages/QuestionBankManagement.vue
Normal file
@ -0,0 +1,648 @@
|
|||||||
|
<template>
|
||||||
|
<div class="question-bank-container">
|
||||||
|
<div class="header-section">
|
||||||
|
<h1 class="title">题库管理</h1>
|
||||||
|
<n-space class="actions-group">
|
||||||
|
<n-button type="primary" @click="addQuestionBank">新建题库</n-button>
|
||||||
|
<n-button ghost @click="importQuestionBank">导入题库</n-button>
|
||||||
|
<n-button ghost @click="exportQuestionBank">导出题库</n-button>
|
||||||
|
<n-button type="error" ghost @click="deleteSelected" :disabled="selectedRowKeys.length === 0">删除</n-button>
|
||||||
|
<n-input
|
||||||
|
v-model:value="filters.keyword"
|
||||||
|
placeholder="请输入想要搜索的内容"
|
||||||
|
style="width: 200px"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<n-button type="primary" @click="searchQuestionBanks">搜索</n-button>
|
||||||
|
</n-space>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 新建/编辑题库弹窗 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showCreateModal"
|
||||||
|
preset="dialog"
|
||||||
|
:title="isEditMode ? '编辑题库' : '新建题库'"
|
||||||
|
style="width: 500px;"
|
||||||
|
>
|
||||||
|
<div class="create-modal-content">
|
||||||
|
<div class="form-item">
|
||||||
|
<label>题库名称:</label>
|
||||||
|
<n-input
|
||||||
|
v-model:value="createForm.name"
|
||||||
|
placeholder="请输入题库名称"
|
||||||
|
style="width: 100%;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-item">
|
||||||
|
<label>题库描述:</label>
|
||||||
|
<n-input
|
||||||
|
v-model:value="createForm.description"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入题库描述"
|
||||||
|
style="width: 100%;"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #action>
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="closeCreateModal">取消</n-button>
|
||||||
|
<n-button type="primary" @click="submitQuestionBank" :disabled="!createForm.name.trim()">
|
||||||
|
{{ isEditMode ? '保存' : '创建' }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
|
||||||
|
<!-- 导入弹窗 -->
|
||||||
|
<ImportModal
|
||||||
|
v-model:show="showImportModal"
|
||||||
|
template-name="question_bank_template.xlsx"
|
||||||
|
import-type="questionBank"
|
||||||
|
@success="handleImportSuccess"
|
||||||
|
@template-download="handleTemplateDownload"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
|
||||||
|
import { NButton, NSpace, useMessage, useDialog } from 'naive-ui';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import ImportModal from '@/components/common/ImportModal.vue';
|
||||||
|
import { ExamApi } from '@/api'
|
||||||
|
|
||||||
|
// 消息提示和对话框
|
||||||
|
const message = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 题库数据接口
|
||||||
|
interface QuestionBank {
|
||||||
|
id: string;
|
||||||
|
sequence: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
questionCount: number;
|
||||||
|
creator: string;
|
||||||
|
createTime: string;
|
||||||
|
lastModified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const filters = reactive({
|
||||||
|
keyword: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 表格相关状态
|
||||||
|
const loading = ref(false);
|
||||||
|
const selectedRowKeys = ref<string[]>([]);
|
||||||
|
const questionBankList = ref<QuestionBank[]>([]);
|
||||||
|
|
||||||
|
// 新建/编辑题库相关状态
|
||||||
|
const showCreateModal = ref(false);
|
||||||
|
const isEditMode = ref(false);
|
||||||
|
const editingId = ref('');
|
||||||
|
const createForm = reactive({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导入弹窗状态
|
||||||
|
const showImportModal = ref(false);
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 创建表格列的函数
|
||||||
|
const createColumns = ({
|
||||||
|
handleAction,
|
||||||
|
}: {
|
||||||
|
handleAction: (action: string, rowData: QuestionBank) => void;
|
||||||
|
}) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'selection',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
key: 'sequence',
|
||||||
|
width: 80,
|
||||||
|
align: 'center' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '题库名称',
|
||||||
|
key: 'name',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: {
|
||||||
|
tooltip: true
|
||||||
|
},
|
||||||
|
render(row: QuestionBank) {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: 'color: #1890ff; cursor: pointer;',
|
||||||
|
onClick: () => handleAction('进入', row)
|
||||||
|
},
|
||||||
|
row.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '题库描述',
|
||||||
|
key: 'description',
|
||||||
|
width: 250,
|
||||||
|
ellipsis: {
|
||||||
|
tooltip: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '题目数量',
|
||||||
|
key: 'questionCount',
|
||||||
|
width: 100,
|
||||||
|
align: 'center' as const,
|
||||||
|
render(row: QuestionBank) {
|
||||||
|
return h('span', { style: 'color: #1890ff; font-weight: 500;' }, row.questionCount.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建人',
|
||||||
|
key: 'creator',
|
||||||
|
width: 100,
|
||||||
|
align: 'center' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
key: 'createTime',
|
||||||
|
width: 160,
|
||||||
|
align: 'center' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后修改',
|
||||||
|
key: 'lastModified',
|
||||||
|
width: 160,
|
||||||
|
align: 'center' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 150,
|
||||||
|
align: 'center' as const,
|
||||||
|
render(row: QuestionBank) {
|
||||||
|
const buttons: VNode[] = [];
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'primary',
|
||||||
|
ghost: true,
|
||||||
|
style: 'margin: 0 3px;',
|
||||||
|
onClick: () => handleAction('进入', row)
|
||||||
|
}, { default: () => '进入' })
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
ghost: true,
|
||||||
|
style: 'margin: 0 3px;',
|
||||||
|
onClick: () => handleAction('编辑', row)
|
||||||
|
}, { default: () => '编辑' })
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'error',
|
||||||
|
ghost: true,
|
||||||
|
style: 'margin: 0 3px;',
|
||||||
|
onClick: () => handleAction('删除', row)
|
||||||
|
}, { default: () => '删除' })
|
||||||
|
);
|
||||||
|
|
||||||
|
return h(NSpace, {}, { default: () => buttons });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = createColumns({
|
||||||
|
handleAction: (action, row) => {
|
||||||
|
if (action === '进入') {
|
||||||
|
enterQuestionBank(row.id);
|
||||||
|
} else if (action === '编辑') {
|
||||||
|
editQuestionBank(row.id);
|
||||||
|
} else if (action === '删除') {
|
||||||
|
deleteQuestionBank(row.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成模拟数据
|
||||||
|
const generateMockData = (): QuestionBank[] => {
|
||||||
|
const mockData: QuestionBank[] = [];
|
||||||
|
const creators = ['王建国', '李明', '张三', '刘老师', '陈教授'];
|
||||||
|
const names = ['计算机基础题库', '数学专业题库', '英语考试题库', '物理练习题库', '化学综合题库'];
|
||||||
|
|
||||||
|
for (let i = 1; i <= 20; i++) {
|
||||||
|
mockData.push({
|
||||||
|
id: '1960998116632399873',
|
||||||
|
sequence: i,
|
||||||
|
name: names[Math.floor(Math.random() * names.length)] + ` ${i}`,
|
||||||
|
description: `这是一个包含多种题型的综合性题库,适用于教学和考试场景...`,
|
||||||
|
questionCount: Math.floor(Math.random() * 500) + 50,
|
||||||
|
creator: creators[Math.floor(Math.random() * creators.length)],
|
||||||
|
createTime: '2025.08.20 09:20',
|
||||||
|
lastModified: '2025.08.28 15:30'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return mockData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理复选框选择
|
||||||
|
const handleCheck = (rowKeys: string[]) => {
|
||||||
|
selectedRowKeys.value = rowKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索题库
|
||||||
|
const searchQuestionBanks = () => {
|
||||||
|
pagination.page = 1;
|
||||||
|
loadQuestionBanks();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载题库列表
|
||||||
|
const loadQuestionBanks = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// TODO API调用
|
||||||
|
// await ExamApi.getCourseRepoList();
|
||||||
|
|
||||||
|
const allData = generateMockData();
|
||||||
|
// 模拟筛选
|
||||||
|
let filteredData = allData;
|
||||||
|
|
||||||
|
if (filters.keyword) {
|
||||||
|
filteredData = filteredData.filter(item =>
|
||||||
|
item.name.includes(filters.keyword) ||
|
||||||
|
item.description.includes(filters.keyword) ||
|
||||||
|
item.creator.includes(filters.keyword)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
pagination.total = filteredData.length;
|
||||||
|
const start = (pagination.page - 1) * pagination.pageSize;
|
||||||
|
const end = start + pagination.pageSize;
|
||||||
|
questionBankList.value = filteredData.slice(start, end);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载题库失败:', error);
|
||||||
|
message.error('加载题库失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 操作方法
|
||||||
|
const addQuestionBank = () => {
|
||||||
|
// 重置为新建模式
|
||||||
|
isEditMode.value = false;
|
||||||
|
editingId.value = '';
|
||||||
|
createForm.name = '';
|
||||||
|
createForm.description = '';
|
||||||
|
showCreateModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importQuestionBank = () => {
|
||||||
|
showImportModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportQuestionBank = () => {
|
||||||
|
console.log('导出题库');
|
||||||
|
message.info('导出功能开发中...');
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSelected = () => {
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
message.warning('请先选择要删除的题库');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中的题库信息
|
||||||
|
const selectedBanks = questionBankList.value.filter(item =>
|
||||||
|
selectedRowKeys.value.includes(item.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const bankNames = selectedBanks.map(bank => bank.name).join('、');
|
||||||
|
|
||||||
|
// 二次确认弹窗
|
||||||
|
dialog.warning({
|
||||||
|
title: '批量删除确认',
|
||||||
|
content: `确定要删除以下 ${selectedRowKeys.value.length} 个题库吗?
|
||||||
|
|
||||||
|
题库名称:${bankNames}
|
||||||
|
|
||||||
|
`,
|
||||||
|
positiveText: '确定删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
// 批量调用删除接口
|
||||||
|
const deletePromises = selectedRowKeys.value.map(id =>
|
||||||
|
ExamApi.deleteRepo(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
|
||||||
|
message.success(`成功删除 ${selectedRowKeys.value.length} 个题库`);
|
||||||
|
|
||||||
|
// 清空选中状态
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
|
||||||
|
// 重新加载列表
|
||||||
|
loadQuestionBanks();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除题库失败:', error);
|
||||||
|
message.error('批量删除失败,请重试');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
// 用户取消删除,不需要做任何操作
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 进入题库(跳转到试题管理页面)
|
||||||
|
const enterQuestionBank = (bankId: string) => {
|
||||||
|
router.push(`/teacher/exam-management/question-bank/${bankId}/questions`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editQuestionBank = (id: string) => {
|
||||||
|
console.log('编辑题库:', id);
|
||||||
|
|
||||||
|
// 查找要编辑的题库数据
|
||||||
|
const questionBank = questionBankList.value.find(item => item.id === id);
|
||||||
|
if (!questionBank) {
|
||||||
|
message.error('未找到要编辑的题库');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置编辑模式
|
||||||
|
isEditMode.value = true;
|
||||||
|
editingId.value = id;
|
||||||
|
|
||||||
|
// 回显数据
|
||||||
|
createForm.name = questionBank.name;
|
||||||
|
createForm.description = questionBank.description;
|
||||||
|
|
||||||
|
// 显示弹窗
|
||||||
|
showCreateModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteQuestionBank = (id: string) => {
|
||||||
|
// 查找要删除的题库信息
|
||||||
|
const questionBank = questionBankList.value.find(item => item.id === id);
|
||||||
|
if (!questionBank) {
|
||||||
|
message.error('未找到要删除的题库');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二次确认弹窗
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除题库"${questionBank.name}"吗?`,
|
||||||
|
positiveText: '确定删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
// 调用删除接口
|
||||||
|
await ExamApi.deleteRepo(id);
|
||||||
|
message.success('题库删除成功');
|
||||||
|
|
||||||
|
// 重新加载列表
|
||||||
|
loadQuestionBanks();
|
||||||
|
|
||||||
|
// 清空选中状态
|
||||||
|
selectedRowKeys.value = selectedRowKeys.value.filter(key => key !== id);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除题库失败:', error);
|
||||||
|
message.error('删除题库失败,请重试');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
// 用户取消删除,不需要做任何操作
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新建/编辑题库相关方法
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
showCreateModal.value = false;
|
||||||
|
isEditMode.value = false;
|
||||||
|
editingId.value = '';
|
||||||
|
createForm.name = '';
|
||||||
|
createForm.description = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统一的提交方法
|
||||||
|
const submitQuestionBank = async () => {
|
||||||
|
if (!createForm.name.trim()) {
|
||||||
|
message.warning('请输入题库名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEditMode.value) {
|
||||||
|
// 编辑模式
|
||||||
|
await ExamApi.updateRepo({
|
||||||
|
id: editingId.value,
|
||||||
|
title: createForm.name,
|
||||||
|
remark: createForm.description
|
||||||
|
});
|
||||||
|
message.success('题库更新成功');
|
||||||
|
} else {
|
||||||
|
// 新建模式
|
||||||
|
await ExamApi.createCourseRepo({
|
||||||
|
title: createForm.name,
|
||||||
|
remark: createForm.description
|
||||||
|
});
|
||||||
|
message.success('题库创建成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeCreateModal();
|
||||||
|
loadQuestionBanks();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交题库失败:', error);
|
||||||
|
message.error(isEditMode.value ? '更新题库失败,请重试' : '创建题库失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理导入成功
|
||||||
|
const handleImportSuccess = (result: any) => {
|
||||||
|
console.log('导入成功:', result);
|
||||||
|
message.success('题库导入成功');
|
||||||
|
loadQuestionBanks();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理模板下载
|
||||||
|
const handleTemplateDownload = (type?: string) => {
|
||||||
|
console.log('下载模板:', type);
|
||||||
|
message.info('模板下载功能开发中...');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadQuestionBanks();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.question-bank-container {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #E6E6E6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-bank-table {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新建题库弹窗样式 */
|
||||||
|
.create-modal-content {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-modal-content .form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-modal-content .form-item label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.header-section {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.question-bank-container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group .n-input {
|
||||||
|
width: 150px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group .n-select {
|
||||||
|
width: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group .n-input {
|
||||||
|
width: 120px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group .n-select {
|
||||||
|
width: 80px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,7 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="question-management-container">
|
<div class="question-management-container">
|
||||||
|
<div class="breadcrumb-section">
|
||||||
|
<n-breadcrumb>
|
||||||
|
<n-breadcrumb-item @click="goToQuestionBank">
|
||||||
|
<n-icon><ChevronBackOutline /></n-icon>
|
||||||
|
题库管理
|
||||||
|
</n-breadcrumb-item>
|
||||||
|
<n-breadcrumb-item>{{ currentBankName }}</n-breadcrumb-item>
|
||||||
|
</n-breadcrumb>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="header-section">
|
<div class="header-section">
|
||||||
<h1 class="title">全部试题</h1>
|
<h1 class="title">{{ currentBankName }} - 试题管理</h1>
|
||||||
<n-space class="actions-group">
|
<n-space class="actions-group">
|
||||||
<n-button type="primary" @click="addQuestion">添加试题</n-button>
|
<n-button type="primary" @click="addQuestion">添加试题</n-button>
|
||||||
<n-button ghost @click="importQuestions">导入</n-button>
|
<n-button ghost @click="importQuestions">导入</n-button>
|
||||||
@ -165,15 +175,27 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
|
import { ref, reactive, computed, onMounted, h, VNode } from 'vue';
|
||||||
import { NButton, NTag, NSpace, useMessage } from 'naive-ui';
|
import { NButton, NTag, NSpace, NBreadcrumb, NBreadcrumbItem, NIcon, useMessage } from 'naive-ui';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { ChevronBackOutline } from '@vicons/ionicons5';
|
||||||
import ImportModal from '@/components/common/ImportModal.vue';
|
import ImportModal from '@/components/common/ImportModal.vue';
|
||||||
|
import { ExamApi } from '@/api';
|
||||||
|
|
||||||
// 消息提示
|
// 消息提示
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
// 路由
|
// 路由
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// 当前题库信息
|
||||||
|
const currentBankId = computed(() => route.params.bankId as string);
|
||||||
|
const currentBankName = ref('加载中...');
|
||||||
|
|
||||||
|
// 返回题库管理页面
|
||||||
|
const goToQuestionBank = () => {
|
||||||
|
router.push('/teacher/exam-management/question-bank');
|
||||||
|
};
|
||||||
|
|
||||||
// 题目数据接口
|
// 题目数据接口
|
||||||
interface Question {
|
interface Question {
|
||||||
@ -241,9 +263,6 @@ const paginationConfig = computed(() => ({
|
|||||||
pageSizes: [10, 20, 50, 100],
|
pageSizes: [10, 20, 50, 100],
|
||||||
showSizePicker: true,
|
showSizePicker: true,
|
||||||
showQuickJumper: true,
|
showQuickJumper: true,
|
||||||
goto: ()=>{
|
|
||||||
return '跳转'
|
|
||||||
},
|
|
||||||
prefix: (info: { itemCount?: number }) => {
|
prefix: (info: { itemCount?: number }) => {
|
||||||
const itemCount = info.itemCount || 0;
|
const itemCount = info.itemCount || 0;
|
||||||
return `共 ${itemCount} 条`;
|
return `共 ${itemCount} 条`;
|
||||||
@ -434,7 +453,7 @@ const loadQuestions = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// 模拟API调用
|
// 模拟API调用
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await ExamApi.getQuestionsByRepo(currentBankId.value);
|
||||||
|
|
||||||
const allData = generateMockData();
|
const allData = generateMockData();
|
||||||
// 模拟筛选
|
// 模拟筛选
|
||||||
@ -466,7 +485,7 @@ const loadQuestions = async () => {
|
|||||||
|
|
||||||
// 操作方法
|
// 操作方法
|
||||||
const addQuestion = () => {
|
const addQuestion = () => {
|
||||||
router.push('/teacher/exam-management/add-question');
|
router.push(`/teacher/exam-management/add-question/${currentBankId.value}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importQuestions = () => {
|
const importQuestions = () => {
|
||||||
@ -496,6 +515,7 @@ const deleteSelected = () => {
|
|||||||
|
|
||||||
const editQuestion = (id: string) => {
|
const editQuestion = (id: string) => {
|
||||||
console.log('编辑题目:', id);
|
console.log('编辑题目:', id);
|
||||||
|
router.push(`/teacher/exam-management/add-question/${currentBankId.value}/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteQuestion = (id: string) => {
|
const deleteQuestion = (id: string) => {
|
||||||
@ -504,9 +524,33 @@ const deleteQuestion = (id: string) => {
|
|||||||
|
|
||||||
// 组件挂载时加载数据
|
// 组件挂载时加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadCurrentBankInfo();
|
||||||
loadQuestions();
|
loadQuestions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 加载当前题库信息
|
||||||
|
const loadCurrentBankInfo = async () => {
|
||||||
|
try {
|
||||||
|
// 模拟API调用获取题库信息
|
||||||
|
// await ;
|
||||||
|
|
||||||
|
// 模拟根据bankId获取题库名称
|
||||||
|
const bankNames: { [key: string]: string } = {
|
||||||
|
'bank_1': '计算机基础题库 1',
|
||||||
|
'bank_2': '数学专业题库 2',
|
||||||
|
'bank_3': '英语考试题库 3',
|
||||||
|
'bank_4': '物理练习题库 4',
|
||||||
|
'bank_5': '化学综合题库 5'
|
||||||
|
};
|
||||||
|
|
||||||
|
currentBankName.value = bankNames[currentBankId.value] || `题库 ${currentBankId.value}`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载题库信息失败:', error);
|
||||||
|
currentBankName.value = '未知题库';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 分类设置相关方法
|
// 分类设置相关方法
|
||||||
const setCategoryForSelected = () => {
|
const setCategoryForSelected = () => {
|
||||||
if (selectedRowKeys.value.length === 0) {
|
if (selectedRowKeys.value.length === 0) {
|
||||||
@ -651,6 +695,21 @@ const deleteCategory = (categoryValue: string) => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.breadcrumb-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-section .n-breadcrumb-item:first-child {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-section .n-breadcrumb-item:first-child:hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
.header-section {
|
.header-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -359,13 +359,13 @@ const getStudentStatusText = (status: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleViewAnswer = (student: StudentExamInfo) => {
|
const handleViewAnswer = (student: StudentExamInfo) => {
|
||||||
// 跳转到答题详情页面
|
// 跳转到答题详情页面(查看模式)
|
||||||
router.push(`/teacher/exam-management/marking-center/answer-detail/${examInfo.value.id}/${student.id}`)
|
router.push(`/teacher/exam-management/marking-center/grading/${examInfo.value.id}/${student.id}?mode=view`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGrade = (student: StudentExamInfo) => {
|
const handleGrade = (student: StudentExamInfo) => {
|
||||||
// 跳转到批阅页面
|
// 跳转到批阅页面(编辑模式)
|
||||||
router.push(`/teacher/exam-management/marking-center/grading/${examInfo.value.id}/${student.id}`)
|
router.push(`/teacher/exam-management/marking-center/grading/${examInfo.value.id}/${student.id}?mode=edit`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportResults = () => {
|
const exportResults = () => {
|
||||||
@ -386,7 +386,7 @@ const importStudents = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
// 搜索已经通过 computed 属性自动实现
|
// 搜索通过 computed 属性实现
|
||||||
message.info('搜索已应用')
|
message.info('搜索已应用')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user