< template >
< main class = "exam-admin" >
< el -tabs v-model ="activeTab" class="workspace-tabs" >
< el -tab -pane label = "考评管理" name = "exams" >
< section class = "assessment-toolbar" aria -label = " 考 试 筛 选 " >
< div class = "assessment-search" > < el -input v-model ="examKeyword" placeholder="请输入考试名称" clearable :prefix-icon="Search" @keyup.enter="loadExams" /><el-button :icon="Search" @click="loadExams" > 搜 索 < / el -button > < / div >
< div class = "toolbar-spacer" / >
< el -button class = "assessment-create" type = "primary" :icon ="Plus" @click ="$router.push('/exam/manage/new')" > 新 建 考 试 < / el -button >
< / section >
< section v-loading ="loading" class="assessment-list" >
< article v-for ="row in exams" :key="row.id" class="assessment-card" >
< div class = "assessment-cover" > < el -icon > < PictureFilled / > < / e l - i c o n > < / d i v >
< div class = "assessment-info" > < h3 > { { row . name } } < / h3 > < p class = "time-line" > < el -icon > < Clock / > < / e l - i c o n > < s p a n c l a s s = " t i m e - l a b e l " > 起 止 时 间 < / s p a n > < s p a n c l a s s = " t i m e - v a l u e " > { { f o r m a t T i m e ( r o w . s t a r t T i m e ) } } ~ { { f o r m a t T i m e ( r o w . e n d T i m e ) } } < / s p a n > < / p > < p > 考 试 时 长 : { { r o w . d u r a t i o n M i n u t e s | | d u r a t i o n B e t w e e n ( r o w ) } } 分 钟 < / p > < / d i v >
< div class = "assessment-status" > < el -tag : type = "statusMeta[row.status]?.tone || 'info'" effect = "light" > { { statusMeta [ row . status ] ? . label || row . status } } < / e l - t a g > < / d i v >
< 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 >
< / article >
< el -empty v -if = " ! exams.length " class = "table-empty" :image-size ="92" description = "暂无考试" / >
< / section >
< / e l - t a b - p a n e >
< el -tab -pane label = "题库管理" name = "questions" >
< section class = "toolbar" aria -label = " 题 库 筛 选 " >
< 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" / >
< / e l - s e l e c t >
< el -input v -model = " questionQuery.keyword " placeholder = "搜索题干" clearable :prefix-icon ="Search" @keyup.enter ="loadQuestions" / >
< el -button :icon ="Search" @click ="loadQuestions" > 查 询 < / el -button >
< div class = "toolbar-spacer" / >
< div class = "toolbar-actions" >
< el -button type = "primary" :icon ="MagicStick" @click ="openAi" > AI 出 题 < / el -button >
< el -dropdown trigger = "click" @command ="downloadTemplate" >
< el -button :icon ="Download" > 下载模板 < el -icon class = "el-icon--right" > < ArrowDown / > < / e l - i c o n > < / e l - b u t t o n >
< template # dropdown >
< el -dropdown -menu >
< el -dropdown -item command = "word" > Word 导入模板 < / e l - d r o p d o w n - i t e m >
< el -dropdown -item command = "excel" > Excel 导入模板 < / 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 >
< / template >
< / e l - d r o p d o w n >
< el -dropdown trigger = "click" @command ="openImportPicker" >
< el -button :icon ="Upload" :loading ="importing" > 批量导入 < el -icon class = "el-icon--right" > < ArrowDown / > < / e l - i c o n > < / e l - b u t t o n >
< template # dropdown >
< el -dropdown -menu >
< el -dropdown -item command = "word" > 导入 Word 文件 < / e l - d r o p d o w n - i t e m >
< el -dropdown -item command = "excel" > 导入 Excel 文件 < / 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 >
< / template >
< / e l - d r o p d o w n >
< el -button type = "primary" :icon ="Plus" @click ="openQuestion()" > 新 增 题 目 < / el -button >
< input ref = "importInput" class = "import-file-input" type = "file" :accept ="importAccept" @change ="handleImportFile" / >
< / div >
< / section >
< el -table v-loading ="loading" :data="pagedQuestions" class="data-table" >
< el -table -column label = "题型" min -width = " 110 " > < template # default = "{ row }" > < el -tag :type ="typeTone[row.type]" > { { typeName [ row . type ] } } < / e l - t a g > < / t e m p l a t e > < / e l - t a b l e - c o l u m n >
< el -table -column prop = "stem" label = "题干" min -width = " 420 " show -overflow -tooltip / >
< el -table -column label = "题目来源" width = "110" > < template # default = "{ row }" > < el -tag : type = "row.sourceType === 'PLATFORM' ? 'success' : 'info'" > { { row . sourceType === 'PLATFORM' ? '平台题库' : '教师自建' } } < / e l - t a g > < / t e m p l a t e > < / e l - t a b l e - c o l u m n >
< el -table -column label = "正确答案" min -width = " 200 " > < template # default = "{ row }" > < el -tooltip :content ="displayAnswer(row.answer)" placement = "top" :show-after ="300" popper -class = " answer -tooltip " :disabled ="!displayAnswer(row.answer)" > < span class = "answer-clip" > { { displayAnswer ( row . answer ) } } < / span > < / e l - t o o l t i p > < / t e m p l a t e > < / e l - t a b l e - c o l u m n >
< el -table -column prop = "updatedAt" label = "更新时间" min -width = " 180 " > < template # default = "{ row }" > { { formatTime ( row . updatedAt ) } } < / template > < / e l - t a b l e - c o l u m n >
< el -table -column label = "操作" min -width = " 160 " fixed = "right" > < template # default = "{ row }" >
< el -button text type = "primary" :icon ="Edit" @click ="openQuestion(row)" > {{ row.sourceType = = = ' PLATFORM ' ? ' 查 看 ' : ' 编 辑 ' }} < / el -button >
< el -button v-if ="row.sourceType !== 'PLATFORM'" text type="danger" :icon="Delete" @click="removeQuestion(row)" > 删 除 < / el -button >
< / template > < / e l - t a b l e - c o l u m n >
< template # empty >
< div class = "table-empty" >
< el -icon class = "empty-icon" >
< FolderOpened / >
< / e l - i c o n >
< p class = "empty-title" > 暂无题目 < / p >
< p class = "empty-tip" > 可通过 “ 新增题目 ” 按钮添加题目 < / p >
< / div >
< / template >
< / e l - t a b l e >
< div v-if ="filteredQuestions.length" class="question-pager" >
< el -pagination
layout = "total, prev, pager, next, sizes, jumper"
: total = "filteredQuestions.length"
v - model : current - page = "questionPager.page"
v - model : page - size = "questionPager.pageSize"
: page - sizes = "[10, 20, 50, 100]"
background
/ >
< / div >
< / e l - t a b - p a n e >
< / e l - t a b s >
< el -dialog v -model = " questionDialog " : title = "questionReadonly ? '查看题目' : (questionForm.id ? '编辑题目' : '新增题目')" width = "760px" class = "teacher-dialog" destroy -on -close >
< el -form ref = "questionFormRef" :model ="questionForm" :rules ="questionRules" label -width = " 92px " :disabled ="questionReadonly" >
< el -form -item label = "题型" prop = "type" > < el -radio -group v-model ="questionForm.type" @change="resetAnswer"><el-radio-button v-for="item in typeOptions" :key="item.value" :label="item.value" > {{ item.label }} < / el -radio -button > < / 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 label = "归属项目" > < el -select v-model ="questionForm.taskKey" placeholder="可选项目" clearable style="width: 100%" > < el -option v -for = " item in projectOptions " :key ="item.taskKey" : label = "`${item.projectName} / ${item.taskName}`" :value ="item.taskKey" / > < / e l - s e l e c t > < / e l - f o r m - i t e m >
< el -form -item label = "题干" prop = "stem" > < el -input v -model = " questionForm.stem " type = "textarea" :rows ="4" maxlength = "2000" show -word -limit / > < / e l - f o r m - i t e m >
< el -form -item v-if ="isChoice" label="选项" >
< div class = "option-editor" >
< div v-for ="(option, index) in questionForm.options" :key="option.label" class="option-row" >
< el -checkbox v -if = " questionForm.type = = = ' MULTIPLE ' " :model-value ="questionForm.answer.includes(option.label)" @ change = "toggleMultiple(option.label, $event)" / >
< el -radio v -else v-model ="singleAnswer" :label="option.label"><span class="option-label" > {{ option.label }} < / span > < / el -radio >
< el -input v -model = " option.content " : placeholder = "`选项 ${option.label}`" / >
< el -button v -if = " questionForm.options.length > 2 " text :icon=" Delete " aria-label=" 删除选项 " @click=" deleteOption ( index ) " / >
< / div >
< el -button v-if ="questionForm.options.length < 8" text type="primary" :icon="Plus" @click="addOption" > 添 加 选 项 < / el -button >
< / div >
< / e l - f o r m - i t e m >
< 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,.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 >
< / e l - f o r m >
< template # footer > < el -button @click ="questionDialog=false" > < el -icon > < Close / > < / e l - i c o n > 取 消 < / e l - b u t t o n > < e l - b u t t o n v - i f = " ! q u e s t i o n R e a d o n l y " t y p e = " p r i m a r y " : l o a d i n g = " s a v i n g " @ c l i c k = " s a v e Q u e s t i o n " > < e l - i c o n > < C h e c k / > < / e l - i c o n > 保 存 < / e l - b u t t o n > < / t e m p l a t e >
< / e l - d i a l o g >
< / main >
< / template >
< script setup >
import { computed , onActivated , onMounted , reactive , ref } from "vue" ;
import { ElMessage , ElMessageBox } from "element-plus" ;
import { useRouter } from "vue-router" ;
import { ArrowDown , Check , Clock , Close , DataAnalysis , Delete , Download , Edit , EditPen , FolderOpened , MagicStick , Paperclip , PictureFilled , Plus , RefreshLeft , Search , Upload } from "@element-plus/icons-vue" ;
import { saveAs } from "file-saver" ;
import * as examApi from "@/api/exam" ;
const router = useRouter ( ) ;
const activeTab = ref ( "exams" ) ;
const loading = ref ( false ) ; const saving = ref ( false ) ; const importing = ref ( false ) ; const questions = ref ( [ ] ) ; const exams = ref ( [ ] ) ; const examKeyword = ref ( "" ) ; const projectOptions = ref ( [ ] ) ;
const questionDialog = ref ( false ) ; const questionFormRef = ref ( ) ; const questionReadonly = ref ( false ) ;
const importInput = ref ( ) ; const importFormat = ref ( "word" ) ;
const importAccept = computed ( ( ) => importFormat . value === "word" ? ".docx" : ".xls,.xlsx" ) ;
const typeOptions = [ { label : "单选题" , value : "SINGLE" } , { label : "多选题" , value : "MULTIPLE" } , { label : "判断题" , value : "TRUE_FALSE" } , { label : "简答题" , value : "SHORT_ANSWER" } ] ;
const typeName = Object . fromEntries ( typeOptions . map ( ( item ) => [ item . value , item . label ] ) ) ;
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 ( "" ) ; // 题目来源前端筛选: PLATFORM=平台题库 / CUSTOM=教师自建(非 PLATFORM)
// 题库列表使用前端分页:后端一次返回全部,前端按页切片
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 filteredQuestions . 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 ( ) ) ;
const questionRules = { type : [ { required : true , message : "请选择题型" } ] , stem : [ { required : true , message : "请输入题干" } ] , answer : [ { required : true , message : "请输入参考答案" } ] } ;
const isChoice = computed ( ( ) => [ "SINGLE" , "MULTIPLE" ] . includes ( questionForm . type ) ) ;
const singleAnswer = computed ( { get : ( ) => questionForm . answer ? . [ 0 ] || "" , set : ( value ) => { questionForm . answer = value ? [ value ] : [ ] ; } } ) ;
onMounted ( ( ) => Promise . all ( [ loadQuestions ( ) , loadExams ( ) , loadProjects ( ) ] ) ) ;
let examsMounted = false ; // KeepAlive 缓存页面:再次进入时刷新考试列表(新建/发布后无需手动刷新),首次激活跳过避免与 onMounted 重复请求
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' ) ; }
function resetAnswer ( ) { questionForm . answer = questionForm . type === "TRUE_FALSE" ? true : questionForm . type === "SHORT_ANSWER" ? "" : [ ] ; }
function toggleMultiple ( label , checked ) { questionForm . answer = checked ? [ ... new Set ( [ ... questionForm . answer , label ] ) ] : questionForm . answer . filter ( ( item ) => item !== label ) ; }
function addOption ( ) { questionForm . options . push ( { label : String . fromCharCode ( 65 + questionForm . options . length ) , content : "" } ) ; }
function deleteOption ( index ) { const [ removed ] = questionForm . options . splice ( index , 1 ) ; questionForm . answer = questionForm . answer . filter ( ( item ) => item !== removed . label ) ; }
async function saveQuestion ( ) { await questionFormRef . value . validate ( ) ; if ( isChoice . value && ( ! questionForm . answer . length || questionForm . options . some ( ( item ) => ! item . content . trim ( ) ) ) ) return ElMessage . warning ( "请补全选项并设置正确答案" ) ; saving . value = true ; try { questionForm . id ? await examApi . updateQuestion ( questionForm . id , questionForm ) : await examApi . createQuestion ( questionForm ) ; ElMessage . success ( "题目已保存" ) ; questionDialog . value = false ; loadQuestions ( ) ; } finally { saving . value = false ; } }
async function removeQuestion ( row ) { await ElMessageBox . confirm ( "删除后无法恢复,确认删除该题目?" , "删除题目" , { type : "warning" } ) ; await examApi . deleteQuestion ( row . id ) ; ElMessage . success ( "已删除" ) ; loadQuestions ( ) ; }
async function uploadMaterial ( { file } ) { const form = new FormData ( ) ; form . append ( "file" , file ) ; const { data } = await examApi . uploadQuestionAttachment ( form ) ; questionForm . attachmentName = data . name ; questionForm . attachmentUrl = data . url ; ElMessage . success ( "资料已上传" ) ; }
function openImportPicker ( format ) { importFormat . value = format ; importInput . value . value = "" ; importInput . value . click ( ) ; }
async function handleImportFile ( event ) { const [ file ] = event . target . files ; if ( ! file ) return ; await doImport ( { file } ) ; }
async function doImport ( { file } ) { importing . value = true ; try { const form = new FormData ( ) ; form . append ( "file" , file ) ; const { data } = await examApi . importQuestions ( form ) ; ElMessage . success ( ` 成功导入 ${ data . length } 道题 ` ) ; loadQuestions ( ) ; } finally { importing . value = false ; if ( importInput . value ) importInput . value . value = "" ; } }
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 ) { 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 ( "/" , "-" ) : "-" ; }
function durationBetween ( row ) { if ( ! row . startTime || ! row . endTime ) return 0 ; return Math . max ( 0 , Math . round ( ( new Date ( row . endTime ) . getTime ( ) - new Date ( row . startTime ) . getTime ( ) ) / 60000 ) ) ; }
< / script >
< style scoped lang = "scss" >
. exam - admin {
color : var ( -- text - strong ) ;
/* ===== 标签页( 极简文字式, tab 文字不换行)===== */
. workspace - tabs {
: deep ( . el - tabs _ _header ) {
margin - bottom : var ( -- sp - 4 ) ;
}
: deep ( . el - tabs _ _nav - wrap : : after ) {
height : 1 px ;
background - color : var ( -- border - color ) ;
}
: deep ( . el - tabs _ _item ) {
flex : 0 0 auto ;
height : 36 px ;
max - width : none ;
padding : 0 var ( -- sp - 4 ) ;
color : var ( -- text - body ) ;
font - size : var ( -- fs - body ) ;
font - weight : 400 ;
white - space : nowrap ;
& : hover {
color : var ( -- text - strong ) ;
}
& . is - active {
color : var ( -- edu - primary ) ;
font - weight : 600 ;
}
}
: deep ( . el - tabs _ _active - bar ) {
height : 2 px ;
background - color : var ( -- edu - primary ) ;
}
}
/* ===== 筛选 / 操作条(扁平,直接落在画布上;外观归 teacher-light.scss 全局接管)===== */
. toolbar {
display : flex ;
flex - wrap : wrap ;
align - items : center ;
gap : var ( -- sp - 3 ) ;
margin - bottom : var ( -- sp - 4 ) ;
. el - select ,
. el - input {
width : var ( -- field - w ) ;
}
}
. toolbar - spacer {
flex : 1 ;
}
. toolbar - actions {
display : flex ;
align - items : center ;
gap : var ( -- sp - 2 ) ;
}
. import - file - input {
display : none ;
}
/* ===== 题库分页(外观继承自 teacher-light.scss 全局样式,仅控制布局)===== */
. question - pager {
display : flex ;
justify - content : flex - end ;
margin - top : var ( -- sp - 4 ) ;
}
/* ===== 数据表格(纯白 + 灰线)===== */
. data - table {
width : 100 % ;
margin - top : 0 ! important ;
background : # ffffff ;
border : 1 px solid var ( -- border - color ) ;
border - radius : var ( -- r - card ) ;
overflow : hidden ;
: deep ( . el - table _ _header - wrapper th ) {
background : # ffffff ;
color : var ( -- text - strong ) ;
font - weight : 600 ;
font - size : var ( -- fs - body ) ;
}
: deep ( . el - table _ _body tr : hover > td ) {
background : # f9fafb ! important ;
}
: deep ( . el - table _ _cell ) {
font - size : var ( -- fs - body ) ;
color : var ( -- text - strong ) ;
border - color : var ( -- border - color ) ;
}
}
/* ===== 考试名称首字徽标(对齐班级/学生/任务分配基准)===== */
. name - cell {
display : inline - flex ;
align - items : center ;
gap : var ( -- sp - 2 ) ;
vertical - align : middle ;
. exam - badge {
flex : 0 0 auto ;
width : 28 px ;
height : 28 px ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
background : var ( -- edu - primary - soft ) ;
color : var ( -- edu - primary ) ;
border - radius : var ( -- r - control ) ;
font - size : var ( -- fs - body ) ;
font - weight : 600 ;
}
. name - text {
font - weight : 600 ;
color : var ( -- text - strong ) ;
}
}
/* ===== 自定义空状态 ===== */
. table - empty {
padding : var ( -- sp - 5 ) 0 ;
display : flex ;
flex - direction : column ;
align - items : center ;
gap : var ( -- sp - 1 ) ;
. empty - icon {
font - size : 28 px ;
color : var ( -- text - muted ) ;
}
. empty - title {
margin : var ( -- sp - 1 ) 0 0 ;
font - size : var ( -- fs - body ) ;
font - weight : 600 ;
color : var ( -- text - body ) ;
}
. empty - tip {
margin : 0 ;
font - size : var ( -- fs - tag ) ;
color : var ( -- text - muted ) ;
}
}
/* ===== 弹窗内选项编辑器(外观由 teacher-light.scss 全局接管 + 本地布局)===== */
. option - editor {
width : 100 % ;
display : flex ;
flex - direction : column ;
gap : var ( -- sp - 2 ) ;
. option - row {
display : flex ;
align - items : center ;
gap : var ( -- sp - 2 ) ;
padding : 10 px 12 px ;
border : 1 px solid var ( -- border - color ) ;
border - radius : var ( -- r - control ) ;
background : # ffffff ;
transition : border - color 0.15 s ;
& : hover {
border - color : var ( -- edu - primary ) ;
}
. el - radio ,
. el - checkbox {
flex : 0 0 auto ;
margin - right : 0 ;
}
. option - label {
flex : 0 0 auto ;
width : 22 px ;
height : 22 px ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
border - radius : 50 % ;
background : var ( -- edu - primary - soft ) ;
color : var ( -- edu - primary ) ;
font - size : 12 px ;
font - weight : 600 ;
margin - left : 4 px ;
}
. el - input {
flex : 1 1 auto ;
min - width : 0 ;
}
. el - button {
flex : 0 0 auto ;
}
}
> . el - button . el - button -- text {
align - self : flex - start ;
margin - top : var ( -- sp - 1 ) ;
}
}
/* ===== 弹窗内表单微调 ===== */
: deep ( . teacher - dialog ) & ,
: deep ( . admin - edit - dialog ) & {
// dialog 内部的 exam-admin 表单间距
}
// dialog 表单整体紧凑化
: deep ( . el - form ) {
. el - form - item {
margin - bottom : 18 px ;
}
. el - form - item : last - child {
margin - bottom : 0 ;
}
}
/* ===== 考评管理卡片列表(布局来自 master, 配色按教育风令牌重制) ===== */
. assessment - toolbar {
display : flex ;
align - items : center ;
gap : var ( -- sp - 3 ) ;
margin - bottom : var ( -- sp - 4 ) ;
. el - input {
width : 240 px ;
}
}
. assessment - search {
display : flex ;
gap : var ( -- sp - 2 ) ;
. el - input {
width : 260 px ;
}
}
. assessment - create {
min - width : 110 px ;
}
. assessment - list {
display : grid ;
gap : var ( -- sp - 4 ) ;
min - height : 220 px ;
}
. assessment - card {
display : grid ;
grid - template - columns : 56 px minmax ( 220 px , 1.4 fr ) auto auto ;
align - items : center ;
gap : var ( -- sp - 4 ) ;
padding : 14 px 20 px ;
border : 1 px solid var ( -- border - color ) ;
border - radius : 12 px ;
background : # ffffff ;
box - shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.06 ) ;
transition : border - color 0.18 s ease ;
& : hover {
border - color : # d1d5db ;
}
}
. assessment - cover {
display : grid ;
width : 56 px ;
height : 56 px ;
place - items : center ;
border - radius : 10 px ;
color : var ( -- edu - primary ) ;
background : var ( -- edu - primary - soft ) ;
. el - icon {
font - size : 26 px ;
}
}
. assessment - info {
min - width : 0 ;
h3 {
margin : 0 0 6 px ;
color : var ( -- text - strong ) ;
font - size : var ( -- fs - card ) ;
font - weight : 700 ;
white - space : nowrap ;
overflow : hidden ;
text - overflow : ellipsis ;
}
p {
margin : 2 px 0 ;
color : var ( -- text - body ) ;
font - size : var ( -- fs - body ) ;
}
. time - line {
display : inline - flex ;
align - items : center ;
gap : 6 px ;
margin : 4 px 0 ;
padding : 4 px 10 px ;
border - radius : 6 px ;
background : var ( -- edu - primary - soft ) ;
. el - icon {
font - size : 15 px ;
color : var ( -- edu - primary ) ;
}
. time - label {
color : var ( -- edu - primary - deep ) ;
font - size : var ( -- fs - body ) ;
}
. time - value {
color : var ( -- text - strong ) ;
font - size : var ( -- fs - body ) ;
font - weight : 600 ;
font - variant - numeric : tabular - nums ;
}
}
}
. assessment - status {
white - space : nowrap ;
}
. assessment - actions {
display : flex ;
flex - wrap : nowrap ;
justify - content : flex - end ;
align - items : center ;
gap : var ( -- sp - 2 ) ;
. el - button + . el - button {
margin - left : 0 ;
}
}
@ media ( max - width : 1120 px ) {
. assessment - card {
grid - template - columns : 56 px minmax ( 220 px , 1 fr ) auto ;
}
. assessment - actions {
grid - column : 2 / - 1 ;
justify - content : flex - start ;
}
}
@ media ( max - width : 760 px ) {
. assessment - toolbar {
align - items : stretch ;
flex - direction : column ;
gap : var ( -- sp - 3 ) ;
}
. assessment - search {
width : 100 % ;
. el - input {
flex : 1 ;
width : auto ;
}
}
. assessment - card {
grid - template - columns : 48 px 1 fr ;
gap : var ( -- sp - 3 ) ;
padding : var ( -- sp - 3 ) ;
}
. assessment - cover {
width : 48 px ;
height : 48 px ;
. el - icon {
font - size : 22 px ;
}
}
. assessment - status ,
. assessment - actions {
grid - column : 1 / - 1 ;
justify - content : flex - start ;
}
}
. option - row {
display : grid ;
grid - template - columns : 28 px minmax ( 0 , 1 fr ) 36 px ;
align - items : center ;
gap : var ( -- sp - 2 ) ;
margin - bottom : var ( -- sp - 2 ) ;
}
. option - label {
font - weight : 700 ;
}
. file - name {
margin - left : var ( -- sp - 3 ) ;
color : var ( -- edu - primary ) ;
}
@ media ( max - width : 960 px ) {
. toolbar - spacer {
flex - basis : 100 % ;
height : 0 ;
}
}
@ media ( max - width : 760 px ) {
. toolbar {
align - items : stretch ;
flex - direction : column ;
}
. toolbar - spacer {
display : none ;
}
. toolbar - actions {
align - items : stretch ;
flex - direction : column ;
width : 100 % ;
}
. toolbar - actions : deep ( . el - dropdown ) {
width : 100 % ;
}
. toolbar . el - input ,
. toolbar . el - select {
width : 100 % ;
}
: deep ( . toolbar . el - button ) {
width : 100 % ;
}
}
}
/* 正确答案列:两行截断省略,全文悬浮 tooltip 查看 */
. answer - clip {
display : - webkit - box ;
- webkit - box - orient : vertical ;
- webkit - line - clamp : 2 ;
overflow : hidden ;
line - height : 1.45 ;
word - break : break - all ;
}
< / style >
< style lang = "scss" >
/* tooltip 弹层 teleport 到 body, 需 non-scoped; 限宽避免长答案撑满屏幕 */
. answer - tooltip . el - popper {
max - width : 460 px ;
line - height : 1.6 ;
word - break : break - all ;
}
< / style >