@ -10,6 +10,15 @@
: ai - manual - name = "resolvedAiManual.name"
: ai - manual - url = "resolvedAiManual.url"
/ >
<!-- 成绩展示 : 无成绩不显示 ; 有老师评分优先显示老师评分 , 否则显示 AI 评分 -- >
< template v-if ="scoreVisible" >
< span v-if ="displayScore !== null" class="task-toolbar-score" :title ="scoreBadgeTitle" >
{ { displayScore } } < small > 分 < / small >
< / span >
< el -button class = "task-toolbar-button" @click ="openScoreDialog" >
< el -icon > < View / > < / e l - i c o n > 查 看 成 绩
< / e l - b u t t o n >
< / template >
<!-- 保存当前进度按钮已注销
< el -button class = "task-toolbar-button task-toolbar-button--save" :loading ="savingProgress" @click ="triggerExistingAction('save')" >
< el -icon > < CircleCheck / > < / e l - i c o n > 保 存 当 前 进 度
@ -25,11 +34,37 @@
< el -icon > < Download / > < / e l - i c o n > 导 出 实 训 成 果
< / e l - b u t t o n >
< / div >
<!-- 成绩弹窗 : 成绩 / 评语 / 参考答案 , 有则展示无则隐藏 -- >
< el -dialog v-model ="scoreDialogVisible" title="实训成绩" width="560px" append-to-body class="task-score-dialog" >
< div class = "task-score-body" >
< section v-if ="hasTeacherScore || hasAiScore" class="task-score-cards" >
< div v-if ="hasTeacherScore" class="task-score-card task-score-card--teacher" >
< p class = "task-score-card__label" > 老师评分 < / p >
< p class = "task-score-card__value" > { { formatScoreValue ( teacherScore ) } } < small > 分 < / small > < / p >
< p v-if ="teacherScoredAt" class="task-score-card__time" > {{ formatScoreTime ( teacherScoredAt ) }} < / p >
< / div >
< div v-if ="hasAiScore" class="task-score-card task-score-card--ai" >
< p class = "task-score-card__label" > AI 评分 < / p >
< p class = "task-score-card__value" > { { formatScoreValue ( aiScore ) } } < small > 分 < / small > < / p >
< / div >
< / section >
< section v-if ="teacherComment" class="task-score-block" >
< p class = "task-score-block__label" > 老师评语 < / p >
< p class = "task-score-block__text" > { { teacherComment } } < / p >
< / section >
< section v-if ="referenceAnswer" class="task-score-block" >
< p class = "task-score-block__label" > 参考答案 < / p >
< p class = "task-score-block__text" > { { referenceAnswer } } < / p >
< / section >
< el -empty v -if = " ! hasScoreData " description = "老师还没有批阅这份实训任务" :image-size ="90" / >
< / div >
< / e l - d i a l o g >
< / template >
< script setup >
import { computed , onMounted , ref , watch } from "vue" ;
import { CircleCheck , Download , RefreshLeft } from "@element-plus/icons-vue" ;
import { CircleCheck , Download , RefreshLeft , View } from "@element-plus/icons-vue" ;
import { ElMessage } from "element-plus" ;
import { getTrainingTaskByKey } from "@/api/trainingTask" ;
import { deleteStudentTrainingAnswer , getStudentTrainingAnswer , saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer" ;
@ -66,6 +101,89 @@ const loadedMaterial = ref({
const loadedTask = ref ( null ) ;
const exporting = ref ( false ) ;
const savingProgress = ref ( false ) ;
/ / = = = = = = = = = = 成 绩 展 示 状 态 = = = = = = = = = =
const scoreAnswer = ref ( null ) ;
const scoreDialogVisible = ref ( false ) ;
/ / 不 同 接 口 对 同 一 分 数 的 命 名 不 一 致 , 统 一 兜 底 取 值 , 避 免 换 了 字 段 名 就 读 不 到 分 数
const teacherScore = computed ( ( ) => pickScore ( scoreAnswer . value , [ "teacherScore" , "teacherTotalScore" ] ) ) ;
const aiScore = computed ( ( ) => pickScore ( scoreAnswer . value , [ "aiAssessmentScore" , "aiTotalScore" , "aiScore" , "score" , "totalScore" ] ) ) ;
const teacherComment = computed ( ( ) => pickText ( scoreAnswer . value , [ "teacherComment" , "teacherComments" , "comment" ] ) ) ;
const teacherScoredAt = computed ( ( ) => String ( scoreAnswer . value ? . teacherScoredAt || "" ) . trim ( ) ) ;
/ / 参 考 答 案 : 约 定 老 师 “ 开 放 推 送 ” 后 任 务 配 置 里 才 携 带 该 字 段 ( 后 端 字 段 暂 未 提 供 , 预 留 读 取 , 提 供 后 自 动 生 效 )
const referenceAnswer = computed ( ( ) => String ( loadedTask . value ? . referenceAnswer || "" ) . trim ( ) ) ;
const hasTeacherScore = computed ( ( ) => teacherScore . value !== null ) ;
const hasAiScore = computed ( ( ) => aiScore . value !== null ) ;
const hasScoreData = computed ( ( ) => hasTeacherScore . value || hasAiScore . value || Boolean ( teacherComment . value ) ) ;
/ / 工 具 栏 入 口 : 分 数 、 评 语 、 参 考 答 案 任 一 存 在 才 显 示
const scoreVisible = computed ( ( ) => hasScoreData . value || Boolean ( referenceAnswer . value ) ) ;
const displayScore = computed ( ( ) => {
if ( hasTeacherScore . value ) return teacherScore . value ;
if ( hasAiScore . value ) return aiScore . value ;
return null ;
} ) ;
const scoreBadgeTitle = computed ( ( ) => ( hasTeacherScore . value ? "老师评分,点击查看成绩详情" : "AI 评分,点击查看成绩详情" ) ) ;
function toScoreNumber ( value ) {
if ( value === null || value === undefined || value === "" ) return null ;
const num = Number ( value ) ;
return Number . isFinite ( num ) ? num : null ;
}
function pickScore ( source , keys ) {
for ( const key of keys ) {
const num = toScoreNumber ( source ? . [ key ] ) ;
if ( num !== null ) return num ;
}
return null ;
}
function pickText ( source , keys ) {
for ( const key of keys ) {
const text = String ( source ? . [ key ] || "" ) . trim ( ) ;
if ( text ) return text ;
}
return "" ;
}
function formatScoreValue ( value ) {
const num = toScoreNumber ( value ) ;
if ( num === null ) return "--" ;
return num . toFixed ( 2 ) . replace ( /\.00$/ , "" ) . replace ( /(\.\d)0$/ , "$1" ) ;
}
function formatScoreTime ( value ) {
return String ( value ) . replace ( "T" , " " ) . slice ( 0 , 16 ) ;
}
async function loadScoreState ( ) {
const scoreTaskKey = resolvedTaskKey . value ;
if ( ! scoreTaskKey ) {
scoreAnswer . value = null ;
console . debug ( "[成绩] 当前页面未解析到 taskKey, 不展示成绩: " , route . path ) ;
return ;
}
try {
const response = await getStudentTrainingAnswer ( scoreTaskKey ) ;
scoreAnswer . value = response ? . data || null ;
if ( ! scoreAnswer . value ) {
console . debug ( ` [成绩] taskKey= ${ scoreTaskKey } 暂无答题记录,不展示成绩 ` ) ;
} else if ( ! pickScore ( scoreAnswer . value , [ "teacherScore" , "teacherTotalScore" ] ) &&
! pickScore ( scoreAnswer . value , [ "aiAssessmentScore" , "aiTotalScore" , "aiScore" , "score" , "totalScore" ] ) ) {
console . debug ( ` [成绩] taskKey= ${ scoreTaskKey } 有答题记录但无评分数据(未提交 / 未批阅 / 未 AI 评分) ` ) ;
}
} catch ( error ) {
/ / 没 有 答 题 记 录 或 接 口 异 常 时 按 “ 无 成 绩 ” 处 理 , 工 具 栏 不 显 示 成 绩 入 口 。
scoreAnswer . value = null ;
console . debug ( ` [成绩] taskKey= ${ scoreTaskKey } 成绩加载失败,不展示成绩 ` , error ? . message || error ) ;
}
}
function openScoreDialog ( ) {
/ / 打 开 时 重 新 拉 取 , 保 证 教 师 刚 评 完 的 分 / 评 语 能 及 时 看 到 。
loadScoreState ( ) ;
scoreDialogVisible . value = true ;
}
const resolvedTaskKey = computed ( ( ) => resolveTrainingTaskKey ( route , props . taskKey ) ) ;
const resolvedMaterial = computed ( ( ) => ( {
name : String ( props . materialName || loadedMaterial . value . name || "" ) ,
@ -107,6 +225,7 @@ async function loadTaskConfig() {
/ / T h e m a t e r i a l b u t t o n k e e p s i t s u n a v a i l a b l e s t a t e w h e n a t a s k h a s n o c o n f i g u r e d c a s e f i l e .
loadedTask . value = null ;
}
loadScoreState ( ) ;
}
async function exportCurrentTrainingOutcome ( ) {
@ -238,10 +357,15 @@ async function saveProgress() {
}
savingProgress . value = true ;
try {
/ *
* 快照写入 dynamicStepAnswers 而不是 step1Answer :
* step1Answer 是新版页面自身的结构化答案 ( 含知识自测 choiceAnswers ) ,
* 用整页 DOM 快照覆盖会直接冲掉学生已保存的答题 , 导致学生端与教师端评阅都回显不出来 。
* /
await saveStudentTrainingAnswer ( resolvedTaskKey . value , {
saveAction : "SAVE" ,
currentStep : 1 ,
step1Answer : JSON . stringify ( {
dynamicStepAnswers : JSON . stringify ( {
type : "legacy-training-snapshot-v3" ,
route : route . path ,
savedAt : new Date ( ) . toISOString ( ) ,
@ -265,7 +389,8 @@ async function saveLegacyTraining() {
await saveStudentTrainingAnswer ( resolvedTaskKey . value , {
saveAction : "SUBMIT" ,
currentStep : 1 ,
step1Answer : JSON . stringify ( {
/* 同上:快照落到 dynamicStepAnswers, 避免覆盖 step1Answer 里的结构化答题 */
dynamicStepAnswers : JSON . stringify ( {
type : "legacy-training-snapshot-v3" ,
route : route . path ,
savedAt : new Date ( ) . toISOString ( ) ,
@ -324,4 +449,29 @@ function exportLegacyTraining() {
. task - toolbar - button -- submit { border - color : # 2563 eb ; color : # fff ; background : # 2563 eb ; }
. task - toolbar - button -- submit : hover { border - color : # 1 d4ed8 ; color : # fff ; background : # 1 d4ed8 ; }
@ media ( max - width : 768 px ) { . training - task - toolbar { justify - content : stretch ; } . training - task - toolbar > * { flex : 1 1 auto ; } }
/* 成绩徽标:琥珀色,区别于蓝色系工具栏按钮 */
. task - toolbar - score {
display : inline - flex ; align - items : center ; gap : 2 px ; height : 34 px ; padding : 0 12 px ;
border - radius : 8 px ; background : # fff7ed ;
border : 1 px solid rgba ( 180 , 83 , 9 , .35 ) ; color : # b45309 ; font - weight : 700 ; font - size : 16 px ; line - height : 1 ;
white - space : nowrap ;
}
. task - toolbar - score small { font - size : var ( -- student - fs - body ) ; font - weight : 600 ; }
/* 成绩弹窗 */
. task - score - body { display : flex ; flex - direction : column ; gap : 16 px ; }
. task - score - cards { display : flex ; gap : 12 px ; }
. task - score - card { flex : 1 ; border - radius : 10 px ; padding : 14 px 16 px ; border : 1 px solid ; }
. task - score - card -- teacher { background : # eff6ff ; border - color : rgba ( 37 , 99 , 235 , .35 ) ; }
. task - score - card -- ai { background : # f0fbf6 ; border - color : rgba ( 43 , 164 , 113 , .35 ) ; }
. task - score - card _ _label { margin : 0 0 4 px ; font - size : 12 px ; color : # 64748 b ; }
. task - score - card _ _value { margin : 0 ; font - size : 28 px ; font - weight : 700 ; line - height : 1.2 ; }
. task - score - card -- teacher . task - score - card _ _value { color : # 1 d4ed8 ; }
. task - score - card -- ai . task - score - card _ _value { color : # 1 f8f62 ; }
. task - score - card _ _value small { font - size : 13 px ; font - weight : 500 ; margin - left : 2 px ; color : # 64748 b ; }
. task - score - card _ _time { margin : 6 px 0 0 ; font - size : 12 px ; color : # 94 a3b8 ; }
. task - score - block { border - radius : 10 px ; border : 1 px solid rgba ( 100 , 116 , 139 , .25 ) ; background : # f8fafc ; padding : 12 px 16 px ; }
. task - score - block _ _label { margin : 0 0 6 px ; font - size : 12 px ; font - weight : 600 ; color : # 475569 ; }
. task - score - block _ _text { margin : 0 ; font - size : 14 px ; line - height : 1.7 ; color : # 1 e293b ; white - space : pre - wrap ; word - break : break - word ; }
< / style >