@ -15,6 +15,7 @@
< div class = "assessment-actions" >
< el -button v-if ="row.status === 'NOT_STARTED'" text type="primary" :icon="RefreshLeft" @click="withdraw(row)" > 撤 回 < / el -button >
< el -button v-if ="['DRAFT','IN_PROGRESS'].includes(row.status)" text type="primary" :icon="Edit" @click="$router.push(`/exam/manage/${row.id}`)" > 编 辑 < / el -button >
< el -button v-if ="row.status === 'DRAFT'" text type="danger" :icon="Delete" @click="removeExam(row)" > 删 除 < / el -button >
< el -button v-if ="row.status === 'IN_PROGRESS'" text type="primary" :icon="RefreshLeft" @click="$router.push(`/exam/review/${row.id}`)" > 指 定 人 回 退 < / el -button >
< template v-if ="row.status === 'ENDED'"><el-button text type="primary" :icon="EditPen" @click="$router.push(`/exam/review/${row.id}`)">评阅</el-button><el-button text type="primary" :icon="DataAnalysis" @click="$router.push(`/exam/analysis/${row.id}`)">分析</el-button><el-dropdown @command="(command) => handleExport(command, row)"><el-button text type="primary" :icon ="Download" > 导 出 < el -icon class = "el-icon--right" > < ArrowDown / > < / e l - i c o n > < / e l - b u t t o n > < t e m p l a t e # d r o p d o w n > < e l - d r o p d o w n - m e n u > < e l - d r o p d o w n - i t e m c o m m a n d = " p a p e r " > 空 白 试 卷 < / e l - d r o p d o w n - i t e m > < e l - d r o p d o w n - i t e m c o m m a n d = " s c o r e s " > 班 级 成 绩 < / e l - d r o p d o w n - i t e m > < e l - d r o p d o w n - i t e m c o m m a n d = " s u b m i s s i o n s " > 全 部 答 卷 < / e l - d r o p d o w n - i t e m > < / e l - d r o p d o w n - m e n u > < / t e m p l a t e > < / e l - d r o p d o w n > < e l - b u t t o n t e x t t y p e = " d a n g e r " : i c o n = " D e l e t e " @ c l i c k = " r e m o v e E x a m ( r o w ) " > 删 除 < / e l - b u t t o n > < / t e m p l a t e >
< / div >
@ -28,6 +29,10 @@
< el -select v-model ="questionQuery.type" placeholder="全部题型" clearable @change="loadQuestions" >
< el -option v -for = " item in typeOptions " :key ="item.value" :label ="item.label" :value ="item.value" / >
< / e l - s e l e c t >
< el -select v-model ="sourceFilter" placeholder="全部来源" clearable @change="resetQuestionPager" style="width: 140px" >
< el -option label = "平台题库" value = "PLATFORM" / >
< el -option label = "教师自建" value = "CUSTOM" / >
< / e l - s e l e c t >
<!-- 全部项目筛选暂时隐藏 -- >
< el -select v-if ="false" v-model="questionQuery.taskKey" placeholder="全部项目" clearable @change="loadQuestions" style="width: 190px" >
< el -option v -for = " item in projectOptions " :key ="item.taskKey" : label = "`${item.projectName} / ${item.taskName}`" :value ="item.taskKey" / >
@ -79,10 +84,10 @@
< / div >
< / template >
< / e l - t a b l e >
< div v-if =" q uestions.length" class="question-pager">
< div v-if =" filteredQ uestions.length" class="question-pager">
< el -pagination
layout = "total, prev, pager, next, sizes, jumper"
: total = " q uestions.length"
: total = " filteredQ uestions.length"
v - model : current - page = "questionPager.page"
v - model : page - size = "questionPager.pageSize"
: page - sizes = "[10, 20, 50, 100]"
@ -112,7 +117,7 @@
< el -form -item v -else -if = " questionForm.type = = = ' TRUE_FALSE ' " label = "正确答案" > < el -radio -group v-model ="questionForm.answer"><el-radio :label="true">正确</el-radio><el-radio :label ="false" > 错 误 < / el -radio > < / e l - r a d i o - g r o u p > < / e l - f o r m - i t e m >
< el -form -item v -else label = "参考答案" prop = "answer" > < el -input v -model = " questionForm.answer " type = "textarea" :rows ="4" / > < / e l - f o r m - i t e m >
< el -form -item v-if ="questionForm.type === 'SHORT_ANSWER'" label="题目资料" >
< el -upload :show-file-list ="false" :http-request ="uploadMaterial" accept = ".pdf,.doc,.docx,. png,.jpg,.jpeg"> < el -button :icon ="Paperclip" > 上传资料 < / e l - b u t t o n > < / e l - u p l o a d >
< el -upload :show-file-list ="false" :http-request ="uploadMaterial" accept = ".pdf,.doc,.docx,. xls,.xlsx,. png,.jpg,.jpeg"> < el -button :icon ="Paperclip" > 上传资料 < / e l - b u t t o n > < / e l - u p l o a d >
< span v-if ="questionForm.attachmentName" class="file-name" > {{ questionForm.attachmentName }} < / span >
< / e l - f o r m - i t e m >
< el -form -item label = "答案解析" > < el -input v -model = " questionForm.analysis " type = "textarea" :rows ="3" / > < / e l - f o r m - i t e m >
@ -141,11 +146,17 @@ const typeName = Object.fromEntries(typeOptions.map((item) => [item.value, item.
const typeTone = { SINGLE : "" , MULTIPLE : "success" , TRUE _FALSE : "warning" , SHORT _ANSWER : "info" } ;
const statusMeta = { DRAFT : { label : "未发布" , tone : "info" } , NOT _STARTED : { label : "未开始" , tone : "" } , IN _PROGRESS : { label : "进行中" , tone : "success" } , ENDED : { label : "已结束" , tone : "warning" } } ;
const questionQuery = reactive ( { type : "" , taskKey : "" , keyword : "" } ) ;
const sourceFilter = ref ( "" ) ; / / 题 目 来 源 前 端 筛 选 : P L A T F O R M = 平 台 题 库 / C U S T O M = 教 师 自 建 ( 非 P L A T F O R M )
/ / 题 库 列 表 使 用 前 端 分 页 : 后 端 一 次 返 回 全 部 , 前 端 按 页 切 片
const questionPager = reactive ( { page : 1 , pageSize : 10 } ) ;
const filteredQuestions = computed ( ( ) => {
const st = sourceFilter . value ;
if ( ! st ) return questions . value ;
return questions . value . filter ( q => st === "PLATFORM" ? q . sourceType === "PLATFORM" : q . sourceType !== "PLATFORM" ) ;
} ) ;
const pagedQuestions = computed ( ( ) => {
const start = ( questionPager . page - 1 ) * questionPager . pageSize ;
return questions . value . slice ( start , start + questionPager . pageSize ) ;
return filteredQ uestions. value . slice ( start , start + questionPager . pageSize ) ;
} ) ;
const emptyQuestion = ( ) => ( { id : null , taskKey : "" , type : "SINGLE" , stem : "" , options : [ { label : "A" , content : "" } , { label : "B" , content : "" } , { label : "C" , content : "" } , { label : "D" , content : "" } ] , answer : [ ] , analysis : "" , attachmentName : "" , attachmentUrl : "" } ) ;
const questionForm = reactive ( emptyQuestion ( ) ) ;
@ -158,6 +169,7 @@ let examsMounted = false; // KeepAlive 缓存页面:再次进入时刷新考
onActivated ( ( ) => { if ( examsMounted ) loadExams ( ) ; examsMounted = true ; } ) ;
async function loadProjects ( ) { try { projectOptions . value = ( await examApi . getQuestionProjects ( ) ) . data || [ ] ; } catch ( e ) { projectOptions . value = [ ] ; } }
async function loadQuestions ( ) { loading . value = true ; try { questionPager . page = 1 ; questions . value = ( await examApi . getQuestions ( questionQuery ) ) . data || [ ] ; } finally { loading . value = false ; } }
function resetQuestionPager ( ) { questionPager . page = 1 ; }
async function loadExams ( ) { loading . value = true ; try { exams . value = ( await examApi . getTeacherExams ( { keyword : examKeyword . value } ) ) . data || [ ] ; } finally { loading . value = false ; } }
function openQuestion ( row ) { questionReadonly . value = row ? . sourceType === 'PLATFORM' ; Object . assign ( questionForm , row ? JSON . parse ( JSON . stringify ( row ) ) : emptyQuestion ( ) ) ; if ( ! Array . isArray ( questionForm . answer ) && isChoice . value ) questionForm . answer = [ ] ; questionDialog . value = true ; }
function openAi ( ) { router . push ( '/exam/ai-generate' ) ; }
@ -175,7 +187,7 @@ async function doImport({ file }) { importing.value = true; try { const form = n
async function downloadTemplate ( format ) { const extension = format === "excel" ? "xlsx" : "docx" ; saveAs ( await examApi . downloadQuestionTemplate ( format ) , ` 考试题库 ${ format === "excel" ? "Excel" : "Word" } 导入模板. ${ extension } ` ) ; }
async function publish ( row ) { await ElMessageBox . confirm ( ` 确认发布“ ${ row . name } ”?发布后将在设定时间自动开始。 ` , "发布考试" , { type : "warning" } ) ; await examApi . publishExam ( row . id ) ; ElMessage . success ( "已发布" ) ; loadExams ( ) ; }
async function withdraw ( row ) { await examApi . withdrawExam ( row . id ) ; ElMessage . success ( "已撤回" ) ; loadExams ( ) ; }
async function removeExam ( row ) { await ElMessageBox . confirm ( ` 确认永久删除“ ${ row . name } ”?仅在已导出全部答卷后允许删除。 ` , "删除考试" , { type : "warning" , confirmButtonText : "确认删除" } ) ; await examApi . deleteExam ( row . id ) ; ElMessage . success ( "已删除" ) ; loadExams ( ) ; }
async function removeExam ( row ) { const message = row . status === "DRAFT" ? ` 确认删除未发布的考试“ ${ row . name } ”?删除后无法恢复。 ` : ` 确认永久删除“ ${ row . name } ”?仅在已导出全部答卷后允许删除。 ` ; await ElMessageBox . confirm ( message , "删除考试" , { type : "warning" , confirmButtonText : "确认删除" } ) ; await examApi . deleteExam ( row . id ) ; ElMessage . success ( "已删除" ) ; loadExams ( ) ; }
async function handleExport ( command , row ) { const actions = { paper : [ examApi . exportExamPaper , ` ${ row . name } 试卷.docx ` ] , scores : [ examApi . exportExamScores , ` ${ row . name } 班级成绩.xlsx ` ] , submissions : [ examApi . exportExamSubmissions , ` ${ row . name } 全部试卷.zip ` ] } ; const [ action , filename ] = actions [ command ] ; saveAs ( await action ( row . id ) , filename ) ; if ( command === "submissions" ) loadExams ( ) ; }
function displayAnswer ( value ) { if ( Array . isArray ( value ) ) return value . join ( "、" ) ; if ( value === true ) return "正确" ; if ( value === false ) return "错误" ; return value || "-" ; }
function formatTime ( value ) { return value ? new Date ( value ) . toLocaleString ( "zh-CN" , { hour12 : false } ) . replaceAll ( "/" , "-" ) : "-" ; }